Kaynak dosyasından binary dosyasına dönüşüm. Bir C programının derlenme aşamaları - 2 (Compilation)
Bir önceki yazımda Preprocessing aşamasından bahsetmiştim. Bu aşamada preprocessor ifadeleri parse edilip çözümlenerek translation veya compilation unit adı verilen bir kod bloğu üretiliyordu. Bu yazımda da Compilation sürecinden bahsedeceğim.
Compilation aşamasında girdi olarak translation unit alınıp çıktı olarak da assembly code (.s) üretilmektedir. Bu assembly code halen okunabilir durumdadır yani kodu açtığınızda genel assembly bilgisiyle kodu anlayabilir durumdasınızdır. Bu kod donanım bağımlı bir koddur.
gcc derleyicisi kullanarak Compilation aşamasını başlatmak için -S komut satırı argümanı kullanılmaktadır. Compilation sürecini örneklemek için Preprocessing sürecinde de kullandığım debug.c ve debug.h dosyalarını kullanacağım.
Aşağıda belirttiğim komut çalıştırıldıktan sonra debug.c dosyasının Compilation süreci tamamlanarak yine kendi ismiyle ancak dosya uzantısı değişmiş olarak debug.s dosyası üretilecektir. Compiler bu aşamada translation unit kod bloğunu parse ederek assembly kodlarına dönüştürmektedir. Bu aşama ile elde edilen kod içeriği yine aşağıda belirtilmiştir.
gcc -S debug.c.file "debug.c"
.text
.section .rodata
.LC0:
.string "DEBUG: "
.LC1:
.string "Logging is started"
.LC2:
.string "Program is started"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
leaq .LC1(%rip), %rdi
call puts@PLT
leaq .LC0(%rip), %rdi
movl $0, %eax
call printf@PLT
leaq .LC2(%rip), %rdi
call puts@PLT
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
Bu kod daha önce de söylediğim gibi donanıma (target architecture) bağlı bir koddur. Buradaki target architecture (bazen host architecture olarak da kullanılabilir) hangi donanım ya da CPU için derleme yapıldığını belirtmektedir. Intel x86–64 bit makinede çalışacak şekilde üretilen assembly kodu ile ARM 32-Bit mimarisinde çalışacak şekilde üretilen assembly kodu birbirinden farklılık gösterecektir. Bu farklılığı göstermek için linux makinemde ARM mimarisi için de aynı kodu aşağıda belirttiğim komut ile derledim ve üretilen assembly kodunu da yine aşağıda paylaştım.
arm-linux-gnueabi-gcc -S debug.c.arch armv5t
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 2
.eabi_attribute 30, 6
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.file "debug.c"
.text
.section .rodata
.align 2
.LC0:
.ascii "DEBUG: \000"
.align 2
.LC1:
.ascii "Logging is started\000"
.align 2
.LC2:
.ascii "Program is started\000"
.text
.align 2
.global main
.syntax unified
.arm
.fpu softvfp
.type main, %function
main:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
push {fp, lr}
add fp, sp, #4
ldr r0, .L3
bl printf
ldr r0, .L3+4
bl puts
ldr r0, .L3
bl printf
ldr r0, .L3+8
bl puts
mov r3, #0
mov r0, r3
pop {fp, pc}
.L4:
.align 2
.L3:
.word .LC0
.word .LC1
.word .LC2
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",%progbits
Görüldüğü gibi aynı kaynak ve başlık dosyaları olmasına rağmen üretilen assembly kodları birbirinden farklılık göstermektedir. Nedeni de hedef mimari (target architecture) farklılığından kaynaklanmaktadır doğal olarak Intel x86–64 Bit mimari için üretilen assembly kodu ile ARM 32 Bit mimari için üretilen kod birbirinden farklı olmaktadır.
Artık anlıyoruz ki gcc’nin en önemli görevi burada translation unit kodunu hedef mimariye göre assembly koduna dönüştürmektir. Doğal olarak da burada karşılaştığı zorluk aynı kodu girdi olarak alıp ARM, Intel, AMD gibi farklı mimariler için bu mimarilerin kendilerine özel olan Instruction Set’lerini refarans alarak doğru assembly kodunu üretmesidir. gcc burada bu zorlukla başa çıkabilmek için bu süreci kendi içinde iki adımda gerçekleştirmektedir. İlk adımda önce translation unit kodunu C-independent data structure yapısına çevirmektedir. Bu yapıya Abstract Syntax Tree (AST) adı verilmektedir. İkinci adımda ise bu AST veri yapısını kullanarak hedef mimari için uygun assembly koduna dönüştürmektedir. Literatürde de bu ilk adıma Compiler Frontend ikinci adıma da Compiler Backend ismi verilmektedir.
Compilation görevi tamamlandıktan sonra artık Assembly süreci ile devam edilmektedir. Bir sonraki yazımda da bu Assembly süreci ile bilgiler vereceğim.
Diğer yazılarımda görüşmek üzere…