19-bo‘lim

Xotira xavfsizligi va nosozliklarni tuzatish

Aniqlanmagan xatti-harakat, eng ko'p uchraydigan xatolar, sanitizerlar, gdb va valgrind, xavfsiz kod yozish qoidalari.

🕑 19 daqiqa o‘qish 📄 1 145 so‘z 👁 3 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Aniqlanmagan xatti-harakat
  2. Sanitizerlar - eng foydali vosita
  3. Massiv chegarasidan chiqish
  4. Xotira sizishi
  5. Ozod qilingandan keyin ishlatish
  6. Valgrind
  7. gdb - nosozliklarni tuzatuvchi
  8. Kompilyator ogohlantirishlari
  9. Klassik xatolar to'plami
  10. 1. Bittaga adashish
  11. 2. = va ==
  12. 3. Nuqtali vergul
  13. 4. Satrga joy yetmasligi
  14. 5. printf format nomuvofiqligi
  15. 6. Mahalliy o'zgaruvchi manzilini qaytarish
  16. 7. Butun sonlarni bo'lish
  17. Xavfsizroq funksiyalar
  18. Xatolarni qaytarish uslubi
  19. Nosozlikni izlash usuli
  20. Xavfsiz kod qoidalari
  21. Xulosa

C sizni xatolardan himoya qilmaydi. Buning o'rniga u sizga to'liq nazorat beradi - va to'liq javobgarlikni ham.

Aniqlanmagan xatti-harakat #

Aniqlanmagan xatti-harakat (UB) nima uchun xavfli? Python / Java Xato aniqlanadi Istisno tashlanadi Siz darhol bilasiz C - yaxshi holat Dastur darhol qulaydi Segmentation fault Xatoni topasiz C - yomon holat Dastur «ishlaydi» Natija noto'g'ri Oylab sezilmaydi Sanitizerlar uchinchi holatni ikkinchisiga aylantiradi Ular xatoni yashirin qolishga qo'ymaydi
Eng xavfli xato - o'zini ko'rsatmaydigan xato
Eng ko'p uchraydigan UB manbalari
XatoMisol
Massiv chegarasidan chiqishm[10] (hajmi 5)
NULL ko'rsatkichni ochish*NULL
Ishga tushirilmagan o'zgaruvchiint x; printf("%d", x);
Ozod qilingandan keyin ishlatishfree(p); *p = 1;
Ikki marta freefree(p); free(p);
Butun son toshib ketishiINT_MAX + 1
Nolga bo'lisha / 0
Mahalliy o'zgaruvchi manzilini qaytarishreturn &mahalliy;
Satrda '\0' yo'qprintf("%s", nolsiz)
%d bilan double chiqarishprintf("%d", 3.14)

Kompilyator UB bo'lmasligini taxmin qiladi va shu asosda optimallashtiradi. Shuning uchun -O2 bilan yig'ilgan dastur -O0 dan butunlay boshqacha ishlashi mumkin.

Sanitizerlar - eng foydali vosita #

Terminal
gcc -std=c17 -g -fsanitize=address,undefined dastur.c -o dastur
./dastur

Massiv chegarasidan chiqish #

C
#include <stdio.h>

int main(void) {
    int ballar[5] = {85, 92, 78, 95, 88};

    printf("%d\n", ballar[10]);      /* chegaradan tashqarida */

    return 0;
}

Oddiy kompilyatsiyada bu axlat son chiqaradi. Sanitizer bilan esa:

Natija
=================================================================
ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffd...
READ of size 4 at 0x7ffd... thread T0
    #0 0x... in main dastur.c:6
Address is located in stack of thread T0 at offset 48 in frame
    #0 0x... in main dastur.c:3
  This frame has 1 object(s):
    [32, 52) 'ballar' (line 4) <== Memory access at offset 48 overflows

Xatoning aniq qatori ko'rsatildi.

Xotira sizishi #

C
#include <stdlib.h>

int main(void) {
    int *massiv = malloc(100 * sizeof(int));
    massiv[0] = 42;

    return 0;      /* free chaqirilmadi */
}
Natija
=================================================================
ERROR: LeakSanitizer: detected memory leaks

Direct leak of 400 byte(s) in 1 object(s) allocated from:
    #0 0x... in malloc
    #1 0x... in main dastur.c:4

Ozod qilingandan keyin ishlatish #

C
#include <stdlib.h>

int main(void) {
    int *massiv = malloc(10 * sizeof(int));

    free(massiv);
    massiv[0] = 42;      /* ozod qilingandan keyin */

    return 0;
}
Natija
ERROR: AddressSanitizer: heap-use-after-free on address 0x...
WRITE of size 4 at 0x... thread T0
    #0 0x... in main dastur.c:7
freed by thread T0 here:
    #1 0x... in main dastur.c:6
Sanitizerlarni har doim yoqing
Terminal
# Ishlab chiqish uchun
gcc -std=c17 -Wall -Wextra -g -fsanitize=address,undefined dastur.c -o dastur

# Ishlab chiqarish uchun
gcc -std=c17 -Wall -Wextra -O2 dastur.c -o dastur

Sanitizer dasturni ~2 barobar sekinlashtiradi va ko'proq xotira talab qiladi - shuning uchun uni faqat ishlab chiqishda ishlating.

Lekin ishlab chiqishda uni doim yoqing. U topadigan xatolarning ko'pchiligini qo'lda topish deyarli imkonsiz.

Valgrind #

Terminal
gcc -std=c17 -g dastur.c -o dastur
valgrind --leak-check=full --show-leak-kinds=all ./dastur
Natija
==12345== HEAP SUMMARY:
==12345==     in use at exit: 400 bytes in 1 blocks
==12345==   total heap usage: 1 allocs, 0 frees, 400 bytes allocated
==12345==
==12345== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345==    by 0x108668: main (dastur.c:4)
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 400 bytes in 1 blocks
SanitizerValgrind
Tezlik~2x sekin~20x sekin
O'rnatishGCC/Clang ichidaalohida o'rnatiladi
Qayta kompilyatsiyakerakkerak emas
Aniqlikjuda yuqorijuda yuqori

gdb - nosozliklarni tuzatuvchi #

Terminal
gcc -std=c17 -g dastur.c -o dastur
gdb ./dastur
BuyruqQisqartmaNima qiladi
runrDasturni ishga tushiradi
break 15b 1515-qatorga to'xtash nuqtasi
break mainb mainFunksiya boshida to'xtaydi
nextnKeyingi qator (funksiyaga kirmaydi)
stepsKeyingi qator (funksiyaga kiradi)
continuecKeyingi to'xtash nuqtasigacha
print xp xO'zgaruvchi qiymatini ko'rsatadi
backtracebtChaqiruvlar zanjiri
listlKodni ko'rsatadi
quitqChiqish
Natija
$ gdb ./dastur
(gdb) break 12
Breakpoint 1 at 0x1169: file dastur.c, line 12.

(gdb) run
Breakpoint 1, main () at dastur.c:12
12          int natija = bol(a, b);

(gdb) print a
$1 = 10

(gdb) print b
$2 = 0

(gdb) next
Program received signal SIGFPE, Arithmetic exception.

(gdb) backtrace
#0  bol (a=10, b=0) at dastur.c:5
#1  main () at dastur.c:12

backtrace - eng foydali buyruq. U dastur qayerda va qanday yo'l bilan qulaganini ko'rsatadi.

Qulagan dasturni tekshirish
Terminal
ulimit -c unlimited          # core fayllarni yoqamiz
./dastur                     # qulaydi
gdb ./dastur core

Yoki to'g'ridan-to'g'ri:

Terminal
gdb --args ./dastur argument1 argument2
(gdb) run

Kompilyator ogohlantirishlari #

Terminal
gcc -std=c17 -Wall -Wextra -Wpedantic -Wshadow -Wconversion dastur.c -o dastur
BayroqNimani topadi
-WallOdatiy ogohlantirishlar
-WextraQo'shimcha ogohlantirishlar
-WpedanticStandartdan chetlanishlar
-WshadowNomlar bir-birini yashirishi
-WconversionMa'lumot yo'qoladigan aylantirishlar
-WerrorOgohlantirishni xatoga aylantiradi
-Werror ni jamoada ishlating
Terminal
gcc -std=c17 -Wall -Wextra -Werror dastur.c -o dastur

Bu kod ogohlantirishsiz yig'ilishini majbur qiladi. Ogohlantirishlar to'planib qolsa, ular orasida haqiqiy xatoni ko'rmay qolasiz.

Qoida: ogohlantirishlar soni doim nol bo'lsin.

Klassik xatolar to'plami #

1. Bittaga adashish #

C
int massiv[5];

for (int i = 0; i <= 5; i++) {    /* XATO: <= */
    massiv[i] = 0;                /* massiv[5] mavjud emas */
}

2. = va == #

C
if (holat = 1) { ... }            /* qiymat berdi, taqqoslamadi */

3. Nuqtali vergul #

C
for (int i = 0; i < 10; i++);     /* sikl bo'sh */
{
    printf("%d\n", i);            /* faqat bir marta */
}

4. Satrga joy yetmasligi #

C
char bufer[10];
strcpy(bufer, "Bu matn juda uzun");    /* bufer to'lib ketdi */

5. printf format nomuvofiqligi #

C
double narx = 99.5;
printf("%d\n", narx);             /* XATO: %f bo'lishi kerak */

6. Mahalliy o'zgaruvchi manzilini qaytarish #

C
char *salomlash(void) {
    char xabar[50] = "Salom";
    return xabar;                 /* funksiya tugagach yo'q bo'ladi */
}

To'g'ri variantlar:

C
/* 1. Dinamik xotira */
char *salomlash(void) {
    char *xabar = malloc(50);
    if (xabar != NULL) strcpy(xabar, "Salom");
    return xabar;                 /* chaqiruvchi free qiladi */
}

/* 2. Buferni chaqiruvchi bersin - eng yaxshi usul */
void salomlash(char *bufer, size_t hajm) {
    snprintf(bufer, hajm, "Salom");
}

/* 3. static (ehtiyot bo'ling: bitta nusxa, ko'p oqimda xavfli) */
const char *salomlash(void) {
    static char xabar[50] = "Salom";
    return xabar;
}

7. Butun sonlarni bo'lish #

C
double ortacha = yigindi / soni;              /* butun bo'lish! */
double togri = (double) yigindi / soni;       /* to'g'ri */

Xavfsizroq funksiyalar #

XavfliXavfsizroqNima uchun
getsfgetsgets butunlay olib tashlangan
strcpysnprintf yoki strncpy + nolHajmni tekshiradi
strcatsnprintfHajmni tekshiradi
sprintfsnprintfHajmni tekshiradi
atoistrtolXatoni aniqlaydi
scanf("%s")fgetsChegara qo'yadi
C
#include <stdio.h>

int main(void) {
    char toliq[20];
    const char *ism = "Husanboy";
    const char *familiya = "Qodirov";

    int yozildi = snprintf(toliq, sizeof(toliq), "%s %s", ism, familiya);

    printf("Natija    : %s\n", toliq);
    printf("Kerak edi : %d belgi\n", yozildi);

    if (yozildi >= (int) sizeof(toliq)) {
        printf("Ogohlantirish: matn kesildi!\n");
    }

    return 0;
}
Natija
Natija    : Husanboy Qodirov
Kerak edi : 16 belgi
snprintf - eng foydali satr funksiyasi

snprintf uchta muammoni bir yo'la hal qiladi:

  1. Hajmdan oshib ketmaydi;
  2. Nolni doim qo'yadi;
  3. Necha belgi kerak bo'lganini qaytaradi.

Qaytgan qiymat bufer hajmidan katta bo'lsa - matn kesilgan degani. Shuning uchun uni tekshirish kifoya:

C
if (snprintf(bufer, hajm, ...) >= (int) hajm) {
    /* joy yetmadi */
}

Xatolarni qaytarish uslubi #

C
#include <stdio.h>

typedef enum {
    XATO_YOQ = 0,
    XATO_NOTOGRI_ARGUMENT,
    XATO_NOLGA_BOLISH,
    XATO_XOTIRA
} Xato;

const char *xato_matni(Xato x) {
    switch (x) {
        case XATO_YOQ:               return "Xato yo'q";
        case XATO_NOTOGRI_ARGUMENT:  return "Noto'g'ri argument";
        case XATO_NOLGA_BOLISH:      return "Nolga bo'lish";
        case XATO_XOTIRA:            return "Xotira yetmadi";
        default:                     return "Noma'lum xato";
    }
}

Xato bol(int a, int b, int *natija) {
    if (natija == NULL) {
        return XATO_NOTOGRI_ARGUMENT;
    }

    if (b == 0) {
        return XATO_NOLGA_BOLISH;
    }

    *natija = a / b;
    return XATO_YOQ;
}

int main(void) {
    int natija;

    Xato x = bol(17, 5, &natija);
    printf("17 / 5: %s", xato_matni(x));
    if (x == XATO_YOQ) printf(" -> %d", natija);
    printf("\n");

    x = bol(17, 0, &natija);
    printf("17 / 0: %s\n", xato_matni(x));

    x = bol(17, 5, NULL);
    printf("NULL  : %s\n", xato_matni(x));

    return 0;
}
Natija
17 / 5: Xato yo'q -> 3
17 / 0: Nolga bo'lish
NULL  : Noto'g'ri argument
C da xatolarni boshqarish uslublari

C da istisnolar (try/catch) yo'q. Uch keng tarqalgan uslub bor:

UslubMisolQachon
Maxsus qiymat qaytarishNULL, -1Oddiy funksiyalar
Xato kodi + natija ko'rsatkichdaYuqoridagi misolAniqlik kerak bo'lsa
Global errnoStandart kutubxonaTizim chaqiruvlari

Ikkinchi uslub eng aniq: qaysi xato ekani ma'lum bo'ladi va natija alohida qaytariladi.

Nosozlikni izlash usuli #

Xatoni topishning tizimli yo'li
  1. Takrorlang. Xato qanday qilib chaqiriladi? Eng kichik misolni toping.
  2. Ogohlantirishlarni o'qing. -Wall -Wextra nima deyapti?
  3. Sanitizerni yoqing. Ko'p xatolar shu bosqichda topiladi.
  4. Chop eting. printf bilan qiymatlarni kuzating - eng oddiy va ko'pincha eng tez usul.
  5. Yarmini kesing. Kodning yarmini o'chirib, xato qoladimi tekshiring. So'ng yana yarmini.
  6. gdb ni ishga soling. backtrace va print bilan holatni ko'ring.
  7. Boshqaga tushuntiring. Ko'pincha tushuntirish paytida xato o'z-o'zidan ko'rinadi.

Oxirgi usul «rezina o'rdak usuli» deb ataladi - va u haqiqatan ishlaydi.

Xavfsiz kod qoidalari #

Har kuni amal qilinadigan qoidalar
#Qoida
1O'zgaruvchini darhol ishga tushiring
2Ko'rsatkichni NULL bilan boshlang
3Ko'rsatkichni ishlatishdan oldin tekshiring
4malloc natijasini tekshiring
5free dan keyin NULL qiling
6Massiv chegarasini doim kuzating
7sizeof dan foydalaning, raqam yozmang
8O'zgartirmasangiz const yozing
9Xavfsizroq funksiyalarni tanlang (snprintf)
10Ogohlantirishlar nol bo'lsin
11Ishlab chiqishda sanitizer yoqing
12Har bir fopen uchun fclose
Amaliy topshiriq
  1. Massiv chegarasidan chiqadigan dastur yozing va sanitizer bilan ishga tushiring.
  2. free ni unutgan dastur yozing va sizishni aniqlang.
  3. free dan keyin xotirani ishlatib, sanitizer xabarini o'qing.
  4. NULL ko'rsatkichni ochib, gdb da backtrace qiling.
  5. -Wall -Wextra -Wconversion bilan eski kodingizni qayta yig'ing.
  6. Nolga bo'lishni gdb da kuzating.
  7. strcpy o'rniga snprintf ishlatib, kesilishni aniqlang.
  8. Xato kodlari uchun enum yarating va funksiyada ishlating.
  9. Mahalliy o'zgaruvchi manzilini qaytaruvchi funksiya yozing va uni to'g'rilang.
  10. O'z kodingizni yuqoridagi 12 qoida bo'yicha tekshiring.

Xulosa #

  • Aniqlanmagan xatti-harakat - dastur «ishlayotgandek» ko'rinishi mumkin, lekin natija ishonchsiz.
  • -fsanitize=address,undefined - eng foydali vosita; ishlab chiqishda doim yoqing.
  • Valgrind qayta kompilyatsiyasiz ishlaydi, lekin sekinroq.
  • gdb da eng muhim buyruq - backtrace.
  • Ogohlantirishlar soni doim nol bo'lsin; jamoada -Werror.
  • Klassik xatolar: bittaga adashish, = va ==, ortiqcha nuqtali vergul, bufer to'lishi, format nomuvofiqligi.
  • Mahalliy o'zgaruvchi manzilini qaytarmang.
  • strcpy va sprintf o'rniga snprintf.
  • C da istisnolar yo'q - xato kodi qaytaring.
  • Xatoni izlashda tizimli boring: takrorlash → ogohlantirish → sanitizer → chop etish → gdb.

Keyingi bo'limda barcha bilimlarni birlashtirib, to'liq amaliy loyiha yaratamiz.

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.