arm-none-eabi-gcc main.c -o firmware.elf — tek satır, tek komut. Ama bu komutun arkasında dört ayrı aşama, bir bağlayıcı betiği ve main() çağrılmadan önce çalışan gizli bir başlangıç kodu var. Derleyicinin gerçekte ne yaptığını anlamadan linker hatalarını okuyamaz, firmware boyutunu kontrol edemez ve kodu nereye yerleştireceğinizi bilemezsiniz.
Bu yazının sonunda şunları anlayacaksınız: GCC'nin dört aşamasının her birinin ne ürettiğini;
arm-none-eabi-gccadının her parçasının ne anlama geldiğini; başlangıç kodunun ve linker betiğinin neden zorunlu olduğunu;.elfdosyasının içinde neler bulunduğunu ve hangi araç zinciri komutlarının ne işe yaradığını.
Bir .c kaynak dosyasından .elf çalıştırılabilir dosyasına giden yol dört adımdan oluşur. Bu aşamaları tek tek çalıştırmak da mümkündür; ancak çoğu zaman hepsini bir arada çalıştıran tek bir komut kullanılır.
Derleyici önce kaynak dosyayı okur ve #include, #define, #ifdef gibi önişlemci direktiflerini genişletir.
// Kaynak dosya
#define SIZE 10
int arr[SIZE];
// Ön işleme sonrası
int arr[10];
Başlık dosyaları doğrudan kaynak koda eklenir, makrolar değerleriyle değiştirilir. Bu aşamada henüz derleme yapılmaz; sonuç saf C kodudur.
Sadece ön işleme çıktısını görmek için:
arm-none-eabi-gcc -E main.c -o main.i
C kodu, hedef mimarinin assembly diline çevrilir.
// C kodu
counter++;
// ARM assembly karşılığı
LDR R0, [counter]
ADD R0, #1
STR R0, [counter]
Bu aşamada tür kontrolü ve optimizasyon gerçekleşir. -O0 optimizasyonu devre dışı bırakır, -O2 agresif optimizasyon uygular. Gömülü sistemlerde özellikle volatile değişkenlerin optimizasyondan nasıl etkilendiğine dikkat etmek gerekir.
Sadece assembly çıktısına ulaşmak için:
arm-none-eabi-gcc -S main.c -o main.s
Assembly kodu, işlemcinin doğrudan anlayabileceği makine koduna (binary) dönüştürülür ve nesne dosyası (.o) üretilir.
1010
1101 → main.o
0101
Nesne dosyaları ham binary'dir; ancak henüz bağlanmamıştır. Semboller (main, SystemInit, vb.) hâlâ çözümlenmemiştir — adresleri bilinmez.
Sadece nesne dosyası üretmek için:
arm-none-eabi-gcc -c main.c -o main.o
arm-none-eabi-gcc -c startup.c -o startup.o
Tüm nesne dosyaları ve kütüphaneler bir araya getirilir, semboller çözümlenir ve nihai çalıştırılabilir dosya üretilir.
arm-none-eabi-gcc main.o startup.o -T linker.ld -o firmware.elf
Bu aşamada linker betiği (-T linker.ld) devreye girer. Linker betiği olmazsa, linker MCU'nun bellek haritasını bilemez ve kodu doğru adreslere yerleştiremez.
arm-none-eabi-gcc Ne Anlama Geliyor?Bu araç zincirinin adı dört parçadan oluşur:
| Parça | Anlam |
|---|---|
arm |
Hedef mimari: ARM |
none |
İşletim sistemi yok — bare-metal |
eabi |
Embedded Application Binary Interface — gömülü sistemler için ABI standardı |
gcc |
GNU Compiler Collection |
Normal gcc komutu, üzerinde çalıştığı bilgisayar için derleme yapar (x86 → x86). arm-none-eabi-gcc ise x86 bir bilgisayarda çalışırken ARM Cortex-M için kod üretir. Buna çapraz derleme (cross-compilation) denir.
PC (x86) MCU (Cortex-M)
│ │
│ gcc ───────────────────── ✗ │
│ │
│ arm-none-eabi-gcc ───────── ✓ │
Kendi kendine derleyemiyen bir MCU'ya kod yazmak için çapraz derleme kaçınılmazdır. STM32, AVR veya ESP32 için yazdığınız C kodunu, ARM veya AVR komut setini anlayan bir çapraz derleyici ile derlemeniz gerekir.
main() Öncesi Ne Olur?Firmware'iniz güç aldığında veya reset geldiğinde, işlemci doğrudan main() fonksiyonunuza atlamaz. Önce başlangıç kodu (startup code) çalışır.
Reset_Handler(void)
{
__set_MSP(); // Stack pointer'ı başlat
SystemInit(); // Sistem saatini yapılandır
// .data bölümünü Flash'tan RAM'e kopyala
// .bss bölümünü sıfırla
__libc_init_array(); // C++ static constructor'larını çalıştır
main(); // Nihayet main() çağrılır
}
Başlangıç kodu dört temel işi yapar:
.data bölümündeki başlangıç değerleri Flash'tan RAM'e kopyalanır; .bss bölümü sıfırlanır.main() fonksiyonuna atlar — Ancak bundan sonra.Başlangıç kodu ve linker betiği olmadan, linker MCU'nun bellek yapısını bilemez. Üreticiler genellikle her MCU ailesi için hazır başlangıç dosyaları (startup_stm32f4xx.s gibi) sağlar.
Linker betiği (.ld dosyası), MCU'nun bellek haritasını tanımlar ve kodun nereye yerleştirileceğini söyler.
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}
SECTIONS
{
.text : { *(.text*) } > FLASH
.data : { *(.data*) } > RAM AT> FLASH
.bss : { *(.bss*) } > RAM
}
Bu betik üç şey yapar:
0x08000000 adresinden başlayan 512 KB Flash0x20000000 adresinden başlayan 128 KB RAM.text (program kodu) Flash'a, .data ve .bss (değişkenler) RAM'eAT> FLASH ifadesi önemlidir: .data bölümü çalışma zamanında RAM'de bulunur, ancak başlangıç değerleri Flash'ta saklanır. Başlangıç kodu bu değerleri çalışma zamanında Flash'tan RAM'e kopyalar.
main.c startup.c
│ │
▼ ▼
Derleme (arm-none-eabi-gcc -c)
│ │
▼ ▼
main.o startup.o ←── Nesne Dosyaları
│ │
▼ ▼
Linker (+ linker.ld)
Sembolleri çözer, bölümleri yerleştirir
│
▼
firmware.elf ←── Çalıştırılabilir Dosya (ELF formatı)
│
▼
objcopy
ELF → binary/hex'e dönüştürür
│
▼
firmware.bin / firmware.hex ←── MCU'ya yüklenir
ELF dosyası debug bilgisi, sembol tablosu ve bölüm bilgilerini içerir. MCU'ya yüklemek için genellikle daha sade formatlar gerekir: .bin (ham binary) veya .hex (Intel HEX formatı).
.elf Dosyasının İçinde Ne Var?ELF (Executable and Linkable Format), yalnızca çalıştırılabilir kodu değil, debug için gerekli tüm bilgileri de barındırır.
| Bölüm | İçerik |
|---|---|
.text |
Program kodu — genellikle Flash'a gider |
.data |
Başlangıç değeri olan global/statik değişkenler |
.bss |
Sıfır başlatılmış global/statik değişkenler |
.rodata |
Salt okunur veriler — const dizeler, sabitler |
.symbols |
Fonksiyon ve değişken adları + adresleri |
.debug |
GDB için debug bilgisi (etkinleştirilmişse) |
ELF dosyası, MCU'nun programlanması, hatasının ayıklanması ve analiz edilmesi için gereken tüm bilgiyi tek pakette sunar. objdump ve nm gibi araçlarla içeriği inceleyebilirsiniz.
# Sadece nesne dosyası üret
arm-none-eabi-gcc -c main.c -o main.o
arm-none-eabi-gcc -c startup.c -o startup.o
# Bağla ve ELF üret
arm-none-eabi-gcc main.o startup.o -T linker.ld -o firmware.elf
# Firmware boyutunu kontrol et (.text + .data + .bss)
arm-none-eabi-size firmware.elf
# Assembly çıktısına bak (disassemble)
arm-none-eabi-objdump -d firmware.elf | less
# ELF → ham binary
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin
# ELF → Intel HEX (çoğu flash aracının beklediği format)
arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
arm-none-eabi-size komutu özellikle pratiktir: Flash ve RAM kullanımınızı anında gösterir. MCU sınırlarına yaklaştığınızda bu komut uyarı niteliğindedir.
GCC build pipeline'ını anlamak, linker hatalarını okumayı, firmware boyutunu yönetmeyi ve kodu kasıtlı olarak doğru bellek bölümlerine yerleştirmeyi mümkün kılar.
| Aşama | Girdi | Çıktı | Araç |
|---|---|---|---|
| Ön işleme | .c |
Genişletilmiş C kodu | gcc -E |
| Derleme | .c |
Assembly (.s) |
gcc -S |
| Birleştirme | .s |
Nesne dosyası (.o) |
gcc -c |
| Bağlama | .o + .ld |
ELF dosyası | gcc -T |
| Dönüştürme | .elf |
.bin / .hex |
objcopy |
Başlangıç kodu main() öncesinde stack, vektör tablosu ve bellek bölümlerini hazırlar. Linker betiği MCU'nun bellek haritasını tanımlar. Bu ikisi olmadan üretilen firmware, işlemcinin anlayabileceği bir düzende değildir.