Ko'pgina dasturchilar stek (stack) xotirasini "tez va bepul" deb hisoblaydi: faqat ko'rsatgichni (stack pointer) surib, joy ajratib olasiz. Ammo bu soddalik yashirin xavfni bergib turadi — stekning oxirgi sahifasi (guard page) o'tib ketish. Agar funksiya bir marta katta hajmdagi xotira so'rasa va ko'rsatgich to'g'ridan-to'g'ri xavfsiz zonadan tashqariga sakrasa, tizim bu o'tishni sezmay qoldirishi mumkin va keyingi to'g'ri sahifaga yozish vaqti xatolikka yo'l qo'yadi.
Nima uchun stek probirovaniya kerak?
Operatsion tizimlar stek uchun ajratilgan virtual xotira blokining oxirida maxsus himoya sahifasi (guard page) belgilaydi. Bu sahifa oqilona huquqqa ega emas — unga kirish harakati protsessor tomonidan signalga aylantiriladi. Tizim bu signalni qabul qilib, stekni kengaytirishi (yangi fizikal sahifa biriktirishi) yoki dasturni to'xtatishi (stack overflow) mumkin.
#include <malloc.h>
void consume(void*,void*);
void f(int n)
{
char buffer[16384];
consume(alloca(n), buffer);
}
Agar funksiya kichik hajmdagi mahalliy o'zgaruvchilar e'lonsa, ko'rsatgich himoya sahifasidan oldin to'xtaydi — xavfsiz. Lekin katta massiv yoki alloca orqali dinamik ajratilganda ko'rsatgich himoya sahifasini "sakrab" o'tishi mumkin. Bu oldini olish uchun kompilyator (MSVC kabi) har bir katta ajratishdan oldin stekni probirovlaydi: himoya sahifasigacha bo'lgan yo'l ustidan o'tib, har bir sahifaga tegib o'tadi. Bu harakat sahifani "dirty" qiladi va OS uni fizikal xotiraga yuklashni majburlaydi.
alloca va oddiy massiv: bir xil miyoya, turli yo'llar
Raymond Chen blogida keltirilgan misolga ko'ra, alloca(n) va katta hajmdagi mahalliy massiv (char buffer[16384]) kompilyator tarafida deyarli identik qabul qilinadi. Ikkalasidan oldin ham __chkstk chaqiriladi.
Farq faqat shundaki: mahalliy massiv uchun hajm kompilyatsiya vaqti bilan aniq, alloca uchun esa run-time'da (ish paytida) belgilanadi. Bu demak, alloca uchun probirovaniya hajmi dinamik hisablanishi kerak.
Assembler darajasida nima sodir bo'ladi? (x86-64)
Quyidagi kod parchasi (MSVC natijasida) ikki marta __chkstk chaqirishining sababini ko'rsatadi:
push rbp
mov eax, 16416 ; 1. Mahalliy freym (buffer + qo'shimcha) uchun probirovaniya
eax qiymati __chkstk ga uzatiladi
call __chkstk
sub rsp, rax ; Haqiqiylikda stek ko'rsatgichini surish
lea rbp, [rsp+32] ; Freym ko'rsatgichini o'rnatish
movsxd rax, ecx ; 2. alloca(n) uchun: n qiymatini rax ga yuklash
lea rcx, [rax+15] ; 16 baytga tekislash (alignment)
and rcx, -16
mov rax, rcx ; __chkstk chaqirish konvansiyasi: hajm rax da bo'lishi kerak
call __chkstk ; Ikkinchi probirovaniya
sub rsp, rcx ; alloca uchun joy ajratish
- Birinchi chaqirish: Funksiya kirishida mahalliy freym (16384 bayt buffer + 32 bayt qo'shimcha = 16416) uchun probirovlanadi.
eax registri hajmni o'tkazadi.
- Ikkinchi chaqirish:
alloca(n) uchun. ecx da kelgan n qiymati 16 baytga tekislanadi (x86-64 ABI talabi). __chkstk maxsus chaqirish konvansiyasiga ega: hajm rax da bo'lishi, natija ham rax da qaytariladi (probirowlangan hajm).
__chkstk chaqirish konvansiyasi va chetlantirish (alignment)
Diqqatga sazovor nuansa: __chkstk oddiy C chaqirish konvansiyasiga (Windows x64: RCX, RDX, R8, R9) rioya etmaydi. Uning o'ziga xos protokoli bor — bu kompilyator ichki funksiyasi (compiler intrinsic) ekanligi dalillati. Hajm RAX orqali o'tkaziladi.
push rbp
mov eax, 16416 ; probe for local frame
call __chkstk
sub rsp, rax ; create local frame
lea rbp, [rsp+32]
movsxd rax, ecx ; n
lea rcx, [rax+15] ; round up to multiple of 16
and rcx, -16
mov rax, rcx ; special __chkstk calling convention
call __chkstk
sub rsp, rcx ; allocate n bytes
lea rdx, [rbp] ; rdx -> buffer
lea rcx, [rsp+32] ; rcx -> alloca'd memory
call consume
lea rsp, [rbp+16384] ; clean up local frame
pop rbp
ret 0
Chetlantirish (alignment) ham avtomatik amalga oshiriladi: lea rcx, [rax+15]; and rcx, -16. Bu n ni 16 baytlik chegaraga yuqtiradi. SIMD buyruqlar (AVX/SSE) va xotira kirish tezligi uchun bu majburiy.
Xavfsizlik va cheklovlar: nima qanday buzoq qilishi mumkin?
Ushbu mexanizim xavfsizlikni ta'minlasa-da, ba'zi cheklovlar va xavflar qoladi:
- Standart emas:
alloca (yoki MSVC da _alloca) C/C++ standartiga kirmaydi. Bu Microsoft, GCC, Clang kengaytmalari. Kod portativligi qanchalik muhim bo'lsa, malloc/free yoki std::vector afzal.
- Iskushtiruvchi xato: Agar
n juda katta bo'lsa (masalan, foydalanuvchi kiritimi tekshirilmagan holda), __chkstk himoya sahifasini topib, Stack Overflow istisnosini (Windows da STATUS_STACK_OVERFLOW) keltirib chiqaradi. Bu dastur to'xtashiga sabab bo'ladi, lekin bu xotira buzilishidan (corruption) ko'ra yaxshiroq.
- Avtomatik tozalash:
alloca bilan ajratilgan xotira funksiya tugagach avtomatik yo'qoladi (ko'rsatgich rbp asosida tiklanadi). Bu qulay, lekin longjmp yoki istisno (exception) paytida stek oqilona tiklanishi kafolatlanishi kerak (MSVC da bu ishlaydi).
- Samaradorlik: Har bir
alloca chaqirishida probirovaniya amalga oshiriladi. Tsikllar ichida katta hajmdagi alloca ishlatish — sekinlashishga yo'l qo'yadi. Bu yerda _malloca (xavfsizroq versiya) yoki tepada (heap) ajratish afzal.
Tizim dasturchisi nima bilishi kerak?
Bu mexanizimni tushunish tizim dasturlash (systems programming), kompilyator tuzish va byudjetli xotira menejmenti qilayotgan har bir muhandis uchun zarur:
- Stek — cheksiz emas. Uning chegarasi himoya sahifasi bilan belgilangan.
- Katta mahalliy o'zgaruvchilar va
alloca kompyulyator tomonidan avtomatik probirovlanadi.
__chkstk bu ishni bajaruvchi yashirin yordamchi — u sahifalarni birma-bir tegib o'tadi.
- Dinamik hajmli stek ajratishi (
alloca) qulay, lekin xavfli: kiritma ma'lumotlarni tekshirish majburiy.
Keyingi safar char buf[20000] yozganda yoki alloca(size) chaqirganda, kompyulyator sizning o'riningizda stekni "tekshirib o'tganini" bilib qoling. Bu — xotira xavfsizligi uchun tushunmasdan amalga oshiriladigan eng murakkab va zaruri operatsiyalardan biri.
Asl manba: devblogs.microsoft.com