19-bo‘lim

Aniqlanmagan xatti-harakat

UB nima, u nima uchun xavfli va sanitayzerlar uni qanday topadi.

🕑 11 daqiqa o‘qish 📄 273 so‘z 👁 3 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. UB nima degani
  2. UBSan - ishorali to'lib ketish
  3. ASan - chegaradan chiqish
  4. ASan - bo'shatilgandan keyin foydalanish
  5. Sizish va boshqa sanitayzerlar
  6. Osilgan havola
  7. Tur qoidalarini buzish
  8. Xavfsiz kod yozish qoidalari
  9. Xulosa

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 #

C++
#include <print>
#include <string>
#include <vector>
#include <limits>
#include <bit>
#include <cstdint>
#include <cstring>
C++
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;
}
Natija
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)
UB - "noto'g'ri javob" emas, "hech qanday javob emas"

Uch xil "aniqlanmagan" bor va ularni ajratish muhim:

AtamaMa'nosi
Aniqlanmagan (undefined)Standart hech narsa kafolatlamaydi
Belgilanmagan (unspecified)Bir necha variantdan biri, qaysi biri - noma'lum
Amalga oshirishga bog'liqKompilyator hujjatlashtiradi

Misollar:

KodToifa
INT_MAX + 1Aniqlanmagan
v[100] (hajmi 5)Aniqlanmagan
Argumentlar hisoblanish tartibiBelgilanmagan
sizeof(int)Amalga oshirishga bog'liq
char ishorali yoki ishorasizmiAmalga 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 #

Terminal
mkdir -p ub && cd ub
Terminal
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|^.*/||'
Natija
--- 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
Sanitayzersiz dastur "ishlagandek" ko'rindi

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:

TekshiruvNima topadi
signed-integer-overflowINT_MAX + 1
shiftx << 40 (32-bitli turda)
nullNol ko'rsatkichni ochish
boundsMassiv chegarasi (statik hajm)
float-cast-overflowdouble dan int ga sig'maydi
vptrNoto'g'ri turga dynamic_cast
returnQiymat qaytarmasdan funksiya tugadi

Hammasini yoqish: -fsanitize=undefined.

ASan - chegaradan chiqish #

Terminal
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
Natija
chiqish kodi nol emasmi: ha

ERROR: AddressSanitizer: heap-buffer-overflow
o'qish xatolari: 1
chegara.cpp:12
ASan chiqishida manzil va PID bor

./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:

KOD
grep -o "ERROR: AddressSanitizer: [a-z-]*"

O'z ishingizda esa albatta to'liq chiqishni o'qing - unda eng qimmatli ma'lumot bor:

Bo'limNima ko'rsatadi
Birinchi qatorXato turi va manzil
Birinchi stekXato qayerda sodir bo'ldi
Ikkinchi stekXotira qayerda ajratildi
Uchinchi stekXotira qayerda bo'shatildi
Shadow bytesBuzilish qanday joylashgan

Ikkinchi va uchinchi steklar - use-after-free ni tekshirishda ajralmas.

ASan - bo'shatilgandan keyin foydalanish #

Terminal
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."
Natija
ERROR: AddressSanitizer: heap-use-after-free
freed by thread
previously allocated by thread

Sabab: push_back xotirani qayta ajratdi va eski blokni bo'shatdi.
Iterator va ko'rsatkichlarni bekor qiluvchi amallar

7-bo'limda konteynerlar bilan tanishgan edik. Endi xavfli tomonini ko'ramiz.

KonteynerQaysi amal bekor qiladi
vectorpush_back, insert, resize, reserve
dequeHar qanday qo'shish/o'chirish
stringappend, +=, resize
listFaqat o'chirilgan element
map, setFaqat o'chirilgan element
unordered_mapRehash (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.

KOD
std::size_t i = 0;              // qayta ajratishdan omon qoladi
int* p = &v[0];                 // omon qolmaydi

Sizish va boshqa sanitayzerlar #

Terminal
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."
Natija
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.
Qaysi sanitayzer nimani topadi -fsanitize=address • Chegaradan chiqish (uyum, stek, global) • Bo'shatilgandan keyin foydalanish • Ikki marta o'chirish • Qamrovdan chiqqan o'zgaruvchi • Xotira sizishi (LeakSanitizer) Narxi: ~2x sekin, ~3x xotira -fsanitize=undefined • Ishorali to'lib ketish • Noto'g'ri siljitish • Nol ko'rsatkichni ochish • Tekislanmagan kirish • Qiymat qaytarmagan funksiya Narxi: ~1.2x sekin -fsanitize=thread • Ma'lumotlar poygasi • Boshi berk ko'cha (deadlock) • Noto'g'ri mutex ishlatilishi Poyga SODIR BO'LMASA ham topadi address bilan BIRGA ishlamaydi Boshqa himoya -Wall -Wextra -Wpedantic -D_GLIBCXX_ASSERTIONS -fstack-protector-strong -Werror (CI da) Bular deyarli bepul - har doim yoqing Amaliy tartib Ishlab chiqish va sinovlarda: address + undefined. Alohida yig'ishda: thread. Ishlab chiqarishda: sanitayzersiz, lekin ogohlantirishlarsiz kod bilan.
Sanitayzerlar - UB ni ishga tushirish paytida ushlaydigan vosita

Osilgan havola #

Terminal
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)"
Natija
--- kompilyator ogohlantiradimi ---
ogohlantirishlar soni: 1
reference to local variable 'mahalliy' returned

--- ASan bilan ishga tushirish ---
ERROR: AddressSanitizer: SEGV
osilgan xotiraga murojaat qilindi: 2
Ogohlantirishlar - birinchi himoya chizig'i

Yuqoridagi xatoni kompilyator o'zi topdi - sanitayzer ham, ishga tushirish ham kerak bo'lmadi.

Shuning uchun minimal to'plam:

KOD
g++ -std=c++23 -Wall -Wextra -Wpedantic

Foydali qo'shimchalar:

BayroqNima topadi
-WshadowIchki qamrovda bir xil nom
-WconversionYashirin tur o'zgarishi
-Wold-style-castC uslubidagi (int)x
-Wnon-virtual-dtorVirtual destruktorsiz bazaviy sinf
-Wnull-dereferenceNol ko'rsatkich
-Wduplicated-condif/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 #

C++
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;
}
Natija
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
Qat'iy taxallus qoidasi (strict aliasing)

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.

KOD
float f = 1.0f;
int* p = (int*)&f;      // UB
*p = 42;                // kompilyator buni f ga aloqasiz deb biladi

To'g'ri yo'llar:

UsulQachon
std::bit_cast<T>(x)C++20, constexpr, eng toza
std::memcpyHar doim ishlaydi, optimallashtiriladi
char*, std::byte*Bu turlar istisno - har narsaga taxallus qilishi mumkin
unionC 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 #

C++
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;
}
Natija
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)
Amaliy topshiriq
  1. INT_MAX + 1 ni hisoblang - sanitayzersiz nima chiqadi?
  2. Xuddi shu kodni -fsanitize=undefined bilan yig'ing.
  3. __builtin_add_overflow bilan xavfsiz tekshiring.
  4. std::vector chegarasidan chiqing va ASan chiqishini o'qing.
  5. push_back dan keyin eski ko'rsatkichni ishlating.
  6. new bilan ajratib delete qilmang - LeakSanitizer nima deydi?
  7. Mahalliy o'zgaruvchiga havola qaytaring - kompilyator ogohlantiradimi?
  8. -Wall -Wextra -Wpedantic ni yoqing va eski kodingizni yig'ing.
  9. reinterpret_cast o'rniga std::bit_cast ishlating.
  10. operator[] ni at() 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_back iterator va ko'rsatkichlarni bekor qiladi - indeks saqlang.
  • Ogohlantirishlar birinchi himoya: -Wall -Wextra -Wpedantic, CI da -Werror.
  • -D_GLIBCXX_ASSERTIONS operator[] ga chegara tekshiruvini qo'shadi.
  • Tur qoidasini buzmang: reinterpret_cast o'rniga std::bit_cast yoki memcpy.

Keyingi, yakuniy bo'limda barcha bilimlarni bitta loyihada birlashtiramiz.

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.