19-bo‘lim

Manba kodidan mashina kodigacha

Kompilyatsiyaning to'rt bosqichi, C kodi assemblerga qanday aylanadi, optimallashtirish nima qiladi va bog'lovchining vazifasi.

🕑 9 daqiqa o‘qish 📄 1 103 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. To'rt bosqich
  2. Kompilyator nima qiladi?
  3. Optimallashtirish turlari
  4. Xulosa

Yozgan kodingiz protsessorga yetib borguncha bir necha bosqichdan o'tadi. Bu bo'limda o'sha yo'lni haqiqiy vositalar bilan ko'rib chiqamiz.

Manba kodidan bajariluvchi faylgacha d.c C manba kodi cpp d.i makroslar ochilgan cc1 d.s assembler matni as d.o mashina kodi ld d dastur Har bosqich nima qiladi Preprotsessor: matn almashtiradi (C tilini tushunmaydi ham) Kompilyator: tahlil qiladi va OPTIMALLASHTIRADI - eng qiyin qism Bog'lovchi: printf kabi havolalarni haqiqiy manzilga ulaydi
gcc hammasini bir chaqiruvda bajaradi - shuning uchun bosqichlar ko'rinmaydi

To'rt bosqich #

Har bosqichni alohida bajarib ko'rish mumkin:

Terminal
gcc -E d.c -o d.i      # 1. preprotsessor
gcc -S d.i -o d.s      # 2. kompilyator
gcc -c d.s -o d.o      # 3. assembler
gcc d.o   -o d         # 4. bog'lovchi

Har bosqich nima qilganini raqamlarda ko'ramiz. Manba fayl atigi 8 qator edi:

BosqichFaylNatija
Asl manbad.c8 qator
Preprotsessordan keyind.i100 dan ortiq qator

Farq qayerdan keldi? #include <stdio.h> qatori butun bir sarlavha faylini ichiga ko'chirdi - undagi yuzlab e'lon bilan birga.

Preprotsessorning o'zi C tilini umuman tushunmaydi. U faqat matn almashtiradi: #include ni fayl mazmuniga, #define ni uning qiymatiga.

Buni ko'rish oson - preprotsessordan keyin main shunday ko'rinadi:

Natija
int main(void) {
    printf("%s, arxitektura!\n", "Salom");
    return 0;
}

#define bilan e'lon qilingan nom o'z qiymatiga almashtirilgan, #include esa yuqorida ochilgan.

Diqqat qiling: SALOM makrosi preprotsessordan keyin yo'qolgan - uning o'rnida "Salom" matni turibdi. Preprotsessor shunchaki matn almashtiradi, C tilini tushunmaydi ham.

BosqichKirishChiqishVositasi
Preprotsessor.c.icpp
Kompilyator.i.scc1
Assembler.s.oas
Bog'lovchi.o + kutubxonalarbajariluvchild

Odatda gcc hammasini bir chaqiruvda bajaradi, shuning uchun bu bosqichlar ko'rinmaydi.

Bog'lovchi nima qiladi?

Sizning .o faylingizda printf yo'q - faqat unga havola bor.

Bog'lovchi shu havolani haqiqiy manzil bilan almashtiradi:

TuriQachonNatija
StatikYig'ish paytidaKutubxona fayl ichiga kiradi
DinamikIshga tushirishdaTizimdagi .so fayl ishlatiladi

Linux da printf odatda dinamik bog'lanadi - shuning uchun kichik dastur ham libc.so ga muhtoj bo'ladi.

12-bo'limdagi assembler dasturimizda esa kutubxona umuman yo'q edi - u to'g'ridan-to'g'ri yadroga murojaat qildi. Shuning uchun u hech narsaga bog'liq emas.

Konteynerlar bilan ishlaganda bu farq muhim bo'ladi: statik bog'langan dastur bo'sh konteynerda ham ishlaydi.

Kompilyator nima qiladi? #

Endi eng qiziq bosqich - C kodi assemblerga aylanishi. Bir xil funksiyani ikki xil optimallashtirish darajasida ko'ramiz:

Bir xil C funksiyasini ikki xil optimallashtirish darajasida kompilyatsiya qilamiz:

Natija
int kvadratlar_yigindisi(int n) {
    int jami = 0;
    for (int i = 1; i <= n; i++) {
        jami += i * i;
    }
    return jami;
}

Natijalarni solishtiramiz:

DarajaKomandalarNishonlarXotiraga murojaat
-O0 (optimallashtirishsiz)1639
-O2 (optimallashtirilgan)1630

Komandalar soni bir xil. Farq faqat oxirgi ustunda - va u hal qiluvchi.

-O0 da har o'zgaruvchi xotirada saqlanadi va har murojaatda o'qib-yoziladi:

Natija
kvadratlar_yigindisi:
   push rbp
   mov rbp, rsp
   mov DWORD PTR -20[rbp], edi
   mov DWORD PTR -4[rbp], 0
   mov DWORD PTR -8[rbp], 1
   jmp .L2
.L3:
   mov eax, DWORD PTR -8[rbp]
   imul eax, eax

-O2 da esa hamma narsa registrda qoladi:

Natija
kvadratlar_yigindisi:
   test edi, edi
   jle .L4
   add edi, 1
   mov eax, 1
   xor edx, edx
.L3:
   mov ecx, eax
   imul ecx, eax
   add eax, 1
   add edx, ecx
   cmp eax, edi
   jne .L3
   mov eax, edx
   ret

[rbp] ko'rinishidagi murojaatlar - bu xotira. Ikkinchi ro'yxatda ular umuman yo'q.

9-bo'limda ko'rgan edik: registrga murojaat bir takt, xotiraga esa yuzlab takt. Kompilyator aynan shu farqni yo'q qildi.

Komandalar soni bir xil chiqdi. Lekin uchinchi ustunga qarang: xotiraga murojaat -O0 da to'qqizta, -O2 da nolta.

Bu 9 va 14-bo'limlardagi bilimning amaliy tasdig'i: optimallashtiruvchi kompilyatorning asosiy ishi - o'zgaruvchilarni registrda ushlab turish.

DarajaNima qiladi
-O0Har o'zgaruvchi xotirada; nosozlik qidirish uchun qulay
-O2Registrlar, halqa optimallashtirish, ortiqchani olib tashlash

-O0 dagi mov DWORD PTR -4[rbp], eax - bu "stekdagi o'zgaruvchiga yozish". -O2 da esa o'sha o'zgaruvchi butun halqa davomida edx registrida qoladi.

godbolt.org - eng foydali o'quv vositasi

Compiler Explorer sayti yozgan kodingiz qanday assemblerga aylanishini darhol ko'rsatadi.

U bilan quyidagilarni sinab ko'ring:

TajribaNimani ko'rasiz
-O0 va -O2 ni solishtirishOptimallashtirish kuchi
if o'rniga arifmetikaShoxsiz kod (16-bo'lim)
x * 8 va x << 3Kompilyator o'zi surishga aylantiradi (4-bo'lim)
Halqa ichidagi doimiyU halqadan tashqariga chiqariladi

Bu "kompilyator meni tushundimi?" degan savolga eng tez javob beradigan usul.

-O2 nosozlik qidirishni qiyinlashtiradi

Optimallashtirilgan kodda:

MuammoSabab
O'zgaruvchi "ko'rinmaydi"U registrda, xotirada yo'q
Qatorlar tartibi chalkashKomandalar qayta tartiblangan
Funksiya yo'qolganU chaqiruv joyiga ko'chirilgan (inline)
Halqa boshqacha ko'rinadiYoyilgan yoki birlashtirilgan

Shuning uchun nosozlik qidirishda -Og ishlatiladi: u ba'zi optimallashtirishlarni qoldiradi, lekin kodni o'qilishli saqlaydi.

Ishlab chiqarishda esa -O2 yoki -O3. -O3 har doim ham tezroq emas - u kodni kattalashtiradi va kesh (14-bo'lim) ga salbiy ta'sir qilishi mumkin. O'lchab ko'ring.

Optimallashtirish turlari #

Kompilyator o'nlab usul qo'llaydi. Eng muhimlari:

UsulNima qiladi
Registrga joylashtirishO'zgaruvchini xotirada emas, registrda saqlash
O'lik kodni olib tashlashHech qachon ishlatilmaydigan kod
Doimiylarni oldindan hisoblash2 * 3 o'rniga 6
Funksiyani ichkariga ko'chirishChaqiruv o'rniga tanasi
Halqani yoyishTakrorlashlar sonini kamaytirish
Kuchni pasaytirishx * 8 o'rniga x << 3

Oxirgi ikkitasi 4 va 16-bo'limlardagi mavzularga bevosita bog'liq.

Aniqlanmagan xulq - optimallashtirishning xavfli tomoni

C va C++ da ba'zi amallar aniqlanmagan xulq (undefined behaviour) hisoblanadi: butun son toshishi, bo'sh ko'rsatkichni o'qish, chegaradan chiqish.

Kompilyator bunday holat hech qachon bo'lmaydi deb faraz qiladi - va shu farazga tayanib optimallashtiradi.

Natijada:

Kod-O0 da-O2 da
Toshish tekshiruviIshlaydiOlib tashlanishi mumkin
Bo'sh ko'rsatkich tekshiruviIshlaydiOlib tashlanishi mumkin

Bu nazariy muammo emas: Linux yadrosida shunday xato bo'lgan - kompilyator bo'sh ko'rsatkich tekshiruvini "keraksiz" deb olib tashlagan va bu zaiflikka olib kelgan.

Xulosa: aniqlanmagan xulqdan qoching. -fsanitize=undefined bayrog'i bilan sinab ko'ring - u bunday joylarni topadi.

3-bo'limdagi butun son toshishi haqidagi ogohlantirish aynan shu mavzuning bir qismi.

Amaliy topshiriq
  1. To'rt bosqichni yoddan tartib bilan yozing.
  2. Har bosqich qanday fayl chiqarishini ayting.
  3. gcc -E bilan o'z faylingizni preprotsessordan o'tkazing.
  4. Natijada necha qator chiqdi? Aslida nechta edi?
  5. gcc -S bilan assembler kodini oling va o'qing.
  6. -O0 va -O2 chiqishlarini solishtiring - qaysi biri qisqa?
  7. [rbp] ko'rinishidagi murojaatlar nimani anglatishini yozing.
  8. -O2 da ular nima uchun yo'qolganini tushuntiring.
  9. godbolt.org da bir funksiyani turli darajalarda sinang.
  10. Statik va dinamik bog'lash farqini ldd bilan ko'ring.

Xulosa #

  • Kompilyatsiya to'rt bosqich: preprotsessor, kompilyator, assembler, bog'lovchi.
  • Preprotsessor shunchaki matn almashtiradi - C tilini tushunmaydi.
  • #include <stdio.h> sakkiz qatorlik faylni yuzlab qatorga aylantiradi.
  • Bog'lovchi printf kabi havolalarni haqiqiy manzil bilan almashtiradi.
  • Bog'lash statik (fayl ichiga) yoki dinamik (ishga tushirishda) bo'ladi.
  • -O0 da har o'zgaruvchi xotirada, -O2 da esa registrda.
  • Bizning misolda -O2 xotiraga murojaatni to'qqizdan nolga tushirdi.
  • Asosiy optimallashtirishlar: registrga joylashtirish, o'lik kod, inline, halqa yoyish.
  • -O3 har doim ham -O2 dan tez emas - kod kattalashib keshga zarar berishi mumkin.
  • Aniqlanmagan xulq kompilyatorga xavfli farazlar qilishga ruxsat beradi.

Keyingi, yakuniy bo'limda 13-bo'limdagi mashinamizga assembler yozamiz va butun zanjirni yopamiz.

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.