19-bo‘lim
Aniqlanmagan xatti-harakat
UB nima, u nima uchun xavfli va sanitayzerlar uni qanday topadi.
Ushbu bo‘lim mundarijasi
C++ dagi eng qo'rqinchli ibora - aniqlanmagan xatti-harakat (undefined behaviour, UB). Bu "xato" emas, "noto'g'ri natija" ham emas. Bu - standart kompilyatorga hech qanday majburiyat yuklamasligi.
UB nima degani #
#include <print>
#include <string>
#include <vector>
#include <limits>
#include <bit>
#include <cstdint>
#include <cstring>
int main()
{
std::println("Turlar chegarasi:");
std::println(" int: {} .. {}",
std::numeric_limits<int>::min(),
std::numeric_limits<int>::max());
std::println(" unsigned: {} .. {}",
std::numeric_limits<unsigned>::min(),
std::numeric_limits<unsigned>::max());
std::println("");
std::println("Ishorasiz TO'LIB KETISH aniqlangan (modul arifmetikasi):");
unsigned u = std::numeric_limits<unsigned>::max();
std::println(" UINT_MAX + 1 = {}", u + 1);
std::println("");
std::println("Ishorali to'lib ketish esa UB.");
std::println("Kompilyator INT_MAX + 1 hech qachon bo'lmaydi deb faraz qiladi.");
std::println("");
std::println("Xavfsiz tekshirish:");
int a = std::numeric_limits<int>::max();
int natija = 0;
bool tolib = __builtin_add_overflow(a, 1, &natija);
std::println(" INT_MAX + 1 to'lib ketdimi: {}", tolib);
tolib = __builtin_add_overflow(100, 200, &natija);
std::println(" 100 + 200 to'lib ketdimi: {} (natija {})", tolib, natija);
return 0;
}
Turlar chegarasi:
int: -2147483648 .. 2147483647
unsigned: 0 .. 4294967295
Ishorasiz TO'LIB KETISH aniqlangan (modul arifmetikasi):
UINT_MAX + 1 = 0
Ishorali to'lib ketish esa UB.
Kompilyator INT_MAX + 1 hech qachon bo'lmaydi deb faraz qiladi.
Xavfsiz tekshirish:
INT_MAX + 1 to'lib ketdimi: true
100 + 200 to'lib ketdimi: false (natija 300)
Uch xil "aniqlanmagan" bor va ularni ajratish muhim:
| Atama | Ma'nosi |
|---|---|
| Aniqlanmagan (undefined) | Standart hech narsa kafolatlamaydi |
| Belgilanmagan (unspecified) | Bir necha variantdan biri, qaysi biri - noma'lum |
| Amalga oshirishga bog'liq | Kompilyator hujjatlashtiradi |
Misollar:
| Kod | Toifa |
|---|---|
INT_MAX + 1 | Aniqlanmagan |
v[100] (hajmi 5) | Aniqlanmagan |
| Argumentlar hisoblanish tartibi | Belgilanmagan |
sizeof(int) | Amalga oshirishga bog'liq |
char ishorali yoki ishorasizmi | Amalga oshirishga bog'liq |
Faqat birinchisi xavfli. UB bo'lgan dasturda kompilyator "bu hech qachon sodir bo'lmaydi" deb faraz qiladi va shu farazga asoslanib kodni o'zgartiradi.
Natijada UB ning oqibati ko'pincha uzoqda, mutlaqo boshqa joyda ko'rinadi.
UBSan - ishorali to'lib ketish #
mkdir -p ub && cd ub
cat > tolish.cpp <<'EOF'
#include <cstdio>
int oshir(int x)
{
return x + 1;
}
int main()
{
int katta = 2147483647; // INT_MAX
std::printf("boshlang'ich: %d\n", katta);
std::printf("natija: %d\n", oshir(katta));
return 0;
}
EOF
echo "--- sanitayzersiz ---"
g++ -std=c++23 -O1 -o tolish tolish.cpp
./tolish
echo ""
echo "--- UBSan bilan ---"
g++ -std=c++23 -O1 -fsanitize=undefined -o tolish_ub tolish.cpp
./tolish_ub 2>&1 | sed 's|^.*/||'
--- sanitayzersiz ---
boshlang'ich: 2147483647
natija: -2147483648
--- UBSan bilan ---
tolish.cpp:5:16: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
boshlang'ich: 2147483647
natija: -2147483648
Birinchi ishga tushirishda dastur hech qanday xato bermadi - shunchaki noto'g'ri son chiqardi.
Aynan shu UB ning xavfi: u jimgina. Sinovlaringiz o'tadi, dastur ishlaydi, keyin kompilyator versiyasi yoki optimallashtirish darajasi o'zgarganda hammasi buziladi.
UBSan esa aniq joyni ko'rsatdi: fayl, qator, ustun va xatoning turi.
Chiqishda UBSan xabari printf qatorlaridan oldin
turganiga e'tibor bering. Bu UB tartibi emas -
buferlash: chiqish quvurga (|) yo'naltirilganda
stdout to'liq buferlanadi va dastur oxirida
yuboriladi, stderr esa buferlanmaydi. Terminalda
to'g'ridan-to'g'ri ishga tushirsangiz tartib boshqacha
bo'ladi.
Asosiy UBSan tekshiruvlari:
| Tekshiruv | Nima topadi |
|---|---|
signed-integer-overflow | INT_MAX + 1 |
shift | x << 40 (32-bitli turda) |
null | Nol ko'rsatkichni ochish |
bounds | Massiv chegarasi (statik hajm) |
float-cast-overflow | double dan int ga sig'maydi |
vptr | Noto'g'ri turga dynamic_cast |
return | Qiymat qaytarmasdan funksiya tugadi |
Hammasini yoqish: -fsanitize=undefined.
ASan - chegaradan chiqish #
cat > chegara.cpp <<'EOF'
#include <cstdio>
#include <vector>
int main()
{
std::vector<int> v{10, 20, 30};
std::printf("v[0] = %d\n", v[0]);
std::printf("v[2] = %d\n", v[2]);
// Chegaradan chiqish - operator[] TEKSHIRMAYDI
std::printf("v[5] = %d\n", v[5]);
return 0;
}
EOF
g++ -std=c++23 -O1 -g -fsanitize=address -o chegara chegara.cpp
./chegara > chiqish.txt 2>&1
kod=$?
echo "chiqish kodi nol emasmi: $([ $kod -ne 0 ] && echo ha || echo yoq)"
echo ""
grep -o "ERROR: AddressSanitizer: [a-z-]*" chiqish.txt
grep -c "READ of size" chiqish.txt | xargs printf "o'qish xatolari: %s\n"
grep -o "chegara.cpp:[0-9]*" chiqish.txt | head -1
chiqish kodi nol emasmi: ha
ERROR: AddressSanitizer: heap-buffer-overflow
o'qish xatolari: 1
chegara.cpp:12
./chegara ning to'liq chiqishi yuzlab qator - unda
xotira manzillari, jarayon raqami va stek izlari bor.
Bu qiymatlar har ishga tushirishda boshqacha
bo'ladi.
Shuning uchun yuqorida biz butun chiqishni emas, faqat barqaror qismlarini ajratib oldik:
grep -o "ERROR: AddressSanitizer: [a-z-]*"
O'z ishingizda esa albatta to'liq chiqishni o'qing - unda eng qimmatli ma'lumot bor:
| Bo'lim | Nima ko'rsatadi |
|---|---|
| Birinchi qator | Xato turi va manzil |
| Birinchi stek | Xato qayerda sodir bo'ldi |
| Ikkinchi stek | Xotira qayerda ajratildi |
| Uchinchi stek | Xotira qayerda bo'shatildi |
| Shadow bytes | Buzilish qanday joylashgan |
Ikkinchi va uchinchi steklar - use-after-free ni
tekshirishda ajralmas.
ASan - bo'shatilgandan keyin foydalanish #
cat > keyin.cpp <<'EOF'
#include <cstdio>
#include <vector>
int main()
{
std::vector<int> v{1, 2, 3};
int* p = &v[0];
std::printf("qayta ajratishdan oldin: %d\n", *p);
for (int i = 0; i < 1000; ++i) v.push_back(i); // qayta ajratish
std::printf("qayta ajratishdan keyin: %d\n", *p); // OSILGAN ko'rsatkich
return 0;
}
EOF
g++ -std=c++23 -O1 -g -fsanitize=address -o keyin keyin.cpp
./keyin > k.txt 2>&1
grep -o "ERROR: AddressSanitizer: [a-z-]*" k.txt
grep -o "freed by thread" k.txt | head -1
grep -o "previously allocated by thread" k.txt | head -1
echo ""
echo "Sabab: push_back xotirani qayta ajratdi va eski blokni bo'shatdi."
ERROR: AddressSanitizer: heap-use-after-free
freed by thread
previously allocated by thread
Sabab: push_back xotirani qayta ajratdi va eski blokni bo'shatdi.
7-bo'limda konteynerlar bilan tanishgan edik. Endi xavfli tomonini ko'ramiz.
| Konteyner | Qaysi amal bekor qiladi |
|---|---|
vector | push_back, insert, resize, reserve |
deque | Har qanday qo'shish/o'chirish |
string | append, +=, resize |
list | Faqat o'chirilgan element |
map, set | Faqat o'chirilgan element |
unordered_map | Rehash (insert ham sabab bo'ladi) |
vector da reserve bilan oldindan joy ajratsangiz,
ko'rsatkichlar sig'im tugagunicha yaroqli qoladi.
Lekin bunga tayanish xavfli - kod o'zgarganda
buziladi.
Xavfsiz yo'l: indeks saqlang, ko'rsatkich emas.
std::size_t i = 0; // qayta ajratishdan omon qoladi
int* p = &v[0]; // omon qolmaydi
Sizish va boshqa sanitayzerlar #
cat > sizish.cpp <<'EOF'
#include <cstdio>
int* yarat(int n)
{
return new int[n]; // hech qachon delete[] qilinmaydi
}
int main()
{
int* a = yarat(100);
a[0] = 42;
std::printf("a[0] = %d\n", a[0]);
int* b = yarat(50);
b[0] = 7;
std::printf("b[0] = %d\n", b[0]);
return 0; // ikkala blok ham sizib chiqdi
}
EOF
g++ -std=c++23 -O1 -g -fsanitize=address -o sizish sizish.cpp
./sizish > s.txt 2>&1
grep -o "ERROR: LeakSanitizer: [a-z ]*" s.txt
grep -o "SUMMARY: AddressSanitizer: [0-9]* byte(s) leaked in [0-9]* allocation(s)" s.txt
echo ""
echo "100 int = 400 bayt, 50 int = 200 bayt, jami 600 bayt."
ERROR: LeakSanitizer: detected memory leaks
SUMMARY: AddressSanitizer: 600 byte(s) leaked in 2 allocation(s)
100 int = 400 bayt, 50 int = 200 bayt, jami 600 bayt.
Osilgan havola #
cat > osilgan.cpp <<'EOF'
#include <cstdio>
#include <string>
const std::string& yomon()
{
std::string mahalliy = "vaqtinchalik";
return mahalliy; // MAHALLIY obyektga havola
}
int main()
{
std::printf("chaqirmoqchimiz...\n");
const std::string& h = yomon();
std::printf("uzunlik: %zu\n", h.size());
return 0;
}
EOF
echo "--- kompilyator ogohlantiradimi ---"
g++ -std=c++23 -Wall -Wextra -O1 -c -o /dev/null osilgan.cpp 2>&1 | grep -c "warning" | xargs printf "ogohlantirishlar soni: %s\n"
g++ -std=c++23 -Wall -Wextra -O1 -c -o /dev/null osilgan.cpp 2>&1 | grep -o "reference to local variable .* returned"
echo ""
echo "--- ASan bilan ishga tushirish ---"
g++ -std=c++23 -O1 -g -fsanitize=address -o osilgan osilgan.cpp 2>/dev/null
./osilgan > o.txt 2>&1
grep -o "ERROR: AddressSanitizer: [A-Za-z-]*" o.txt
echo "osilgan xotiraga murojaat qilindi: $(grep -c "SEGV" o.txt)"
--- kompilyator ogohlantiradimi ---
ogohlantirishlar soni: 1
reference to local variable 'mahalliy' returned
--- ASan bilan ishga tushirish ---
ERROR: AddressSanitizer: SEGV
osilgan xotiraga murojaat qilindi: 2
Yuqoridagi xatoni kompilyator o'zi topdi - sanitayzer ham, ishga tushirish ham kerak bo'lmadi.
Shuning uchun minimal to'plam:
g++ -std=c++23 -Wall -Wextra -Wpedantic
Foydali qo'shimchalar:
| Bayroq | Nima topadi |
|---|---|
-Wshadow | Ichki qamrovda bir xil nom |
-Wconversion | Yashirin tur o'zgarishi |
-Wold-style-cast | C uslubidagi (int)x |
-Wnon-virtual-dtor | Virtual destruktorsiz bazaviy sinf |
-Wnull-dereference | Nol ko'rsatkich |
-Wduplicated-cond | if/else if da bir xil shart |
Uzluksiz integratsiyada -Werror qo'shing -
ogohlantirish yig'ilib qolmasin.
Va -D_GLIBCXX_ASSERTIONS ni unutmang: u
std::vector::operator[] ga chegara tekshiruvini
qo'shadi - deyarli bepul.
Tur qoidalarini buzish #
int main()
{
float f = 3.14159f;
std::println("float qiymat: {}", f);
// NOTO'G'RI: reinterpret_cast bilan tur qoidasini buzish
// std::uint32_t* p = reinterpret_cast<std::uint32_t*>(&f);
// std::println("{}", *p); <- qat'iy taxallus qoidasini buzadi
// TO'G'RI 1: std::bit_cast (C++20)
auto bitlar = std::bit_cast<std::uint32_t>(f);
std::println("bit ko'rinishi: {:#010x}", bitlar);
std::println("ikkilikda: {:032b}", bitlar);
// TO'G'RI 2: std::memcpy - kompilyator uni optimallashtiradi
std::uint32_t nusxa{};
std::memcpy(&nusxa, &f, sizeof(f));
std::println("memcpy natijasi bir xilmi: {}", nusxa == bitlar);
std::println("");
std::println("Orqaga aylantirish:");
float qayta = std::bit_cast<float>(bitlar);
std::println(" qayta olingan: {}", qayta);
std::println(" bir xilmi: {}", qayta == f);
std::println("");
std::println("IEEE 754 qismlari:");
std::println(" ishora: {}", (bitlar >> 31) & 1u);
std::println(" daraja: {}", (bitlar >> 23) & 0xFFu);
std::println(" mantissa: {:#x}", bitlar & 0x7FFFFFu);
return 0;
}
float qiymat: 3.14159
bit ko'rinishi: 0x40490fd0
ikkilikda: 01000000010010010000111111010000
memcpy natijasi bir xilmi: true
Orqaga aylantirish:
qayta olingan: 3.14159
bir xilmi: true
IEEE 754 qismlari:
ishora: 0
daraja: 128
mantissa: 0x490fd0
C++ standarti aytadi: bitta xotira joyiga ikki xil turdagi ko'rsatkich orqali kirish - UB (bir necha istisno bilan).
Sabab: kompilyator float* va int* hech qachon bir
joyga ishora qilmaydi deb faraz qiladi va shu asosda
o'qish/yozishlarni qayta tartiblaydi.
float f = 1.0f;
int* p = (int*)&f; // UB
*p = 42; // kompilyator buni f ga aloqasiz deb biladi
To'g'ri yo'llar:
| Usul | Qachon |
|---|---|
std::bit_cast<T>(x) | C++20, constexpr, eng toza |
std::memcpy | Har doim ishlaydi, optimallashtiriladi |
char*, std::byte* | Bu turlar istisno - har narsaga taxallus qilishi mumkin |
union | C da to'g'ri, C++ da UB |
Oxirgi qator ko'p dasturchini hayron qoldiradi:
union orqali turni o'zgartirish C tilida
aniqlangan, C++ da esa yo'q. GCC uni qo'llab-quvvatlaydi,
lekin standart kafolatlamaydi.
std::bit_cast ni ishlating - u memcpy bilan bir xil
tez, lekin constexpr va turlar hajmini kompilyatsiya
paytida tekshiradi.
Xavfsiz kod yozish qoidalari #
int main()
{
std::vector<int> v{10, 20, 30};
std::println("Chegara tekshiruvi:");
std::println(" v[1] = {} (tekshirmaydi)", v[1]);
std::println(" v.at(1) = {} (tekshiradi)", v.at(1));
try {
std::println(" v.at(99) = {}", v.at(99));
} catch (const std::out_of_range& x) {
std::println(" v.at(99) -> istisno tutildi");
}
std::println("");
std::println("Bo'sh konteyner:");
std::vector<int> bosh;
std::println(" bosh.empty() = {}", bosh.empty());
std::println(" bosh.size() = {}", bosh.size());
std::println(" bosh.front() = UB - CHAQIRMANG");
std::println("");
std::println("Xavfsiz naqshlar:");
if (!bosh.empty()) {
std::println(" birinchi: {}", bosh.front());
} else {
std::println(" konteyner bo'sh - front() chaqirilmadi");
}
std::println("");
std::println("Diapazon bo'yicha tsikl - indeks umuman yo'q:");
for (int x : v) std::print(" {}", x);
std::println("");
std::println("");
std::println("Ko'rsatkich o'rniga indeks:");
std::size_t joy = 1;
v.push_back(40); // qayta ajratish bo'lishi mumkin
std::println(" v[{}] = {} (indeks omon qoldi)", joy, v[joy]);
return 0;
}
Chegara tekshiruvi:
v[1] = 20 (tekshirmaydi)
v.at(1) = 20 (tekshiradi)
v.at(99) -> istisno tutildi
Bo'sh konteyner:
bosh.empty() = true
bosh.size() = 0
bosh.front() = UB - CHAQIRMANG
Xavfsiz naqshlar:
konteyner bo'sh - front() chaqirilmadi
Diapazon bo'yicha tsikl - indeks umuman yo'q:
10 20 30
Ko'rsatkich o'rniga indeks:
v[1] = 20 (indeks omon qoldi)
INT_MAX + 1ni hisoblang - sanitayzersiz nima chiqadi?- Xuddi shu kodni
-fsanitize=undefinedbilan yig'ing. __builtin_add_overflowbilan xavfsiz tekshiring.std::vectorchegarasidan chiqing va ASan chiqishini o'qing.push_backdan keyin eski ko'rsatkichni ishlating.newbilan ajratibdeleteqilmang - LeakSanitizer nima deydi?- Mahalliy o'zgaruvchiga havola qaytaring - kompilyator ogohlantiradimi?
-Wall -Wextra -Wpedanticni yoqing va eski kodingizni yig'ing.reinterpret_casto'rnigastd::bit_castishlating.operator[]niat()ga almashtiring va farqni ko'ring.
Xulosa #
- UB - "noto'g'ri javob" emas, standart hech narsa kafolatlamaydi.
- Kompilyator UB hech qachon sodir bo'lmaydi deb faraz qiladi va kodni shunga qarab o'zgartiradi.
- Ishorasiz to'lib ketish aniqlangan, ishorali esa UB.
- UB jimgina - sinovlar o'tadi, keyin kompilyator versiyasi o'zgarganda buziladi.
- UBSan (
-fsanitize=undefined) fayl, qator va xato turini ko'rsatadi. - ASan (
-fsanitize=address) chegara, use-after-free, ikki marta o'chirish va sizishni topadi. - TSan (
-fsanitize=thread) poygani u sodir bo'lmagan bo'lsa ham topadi. - ASan va TSan birga ishlamaydi - alohida yig'ing.
vector::push_backiterator va ko'rsatkichlarni bekor qiladi - indeks saqlang.- Ogohlantirishlar birinchi himoya:
-Wall -Wextra -Wpedantic, CI da-Werror. -D_GLIBCXX_ASSERTIONSoperator[]ga chegara tekshiruvini qo'shadi.- Tur qoidasini buzmang:
reinterpret_casto'rnigastd::bit_castyokimemcpy.
Keyingi, yakuniy bo'limda barcha bilimlarni bitta loyihada birlashtiramiz.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.