← Tüm makaleler

GCC Build Pipeline: C Kodundan Firmware'e

Dört aşama, çapraz derleme, linker betiği ve başlangıç kodu — arm-none-eabi-gcc gerçekte ne yapar?

GCC Build Pipeline: C Kodundan Firmware'e

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-gcc adının her parçasının ne anlama geldiğini; başlangıç kodunun ve linker betiğinin neden zorunlu olduğunu; .elf dosyasının içinde neler bulunduğunu ve hangi araç zinciri komutlarının ne işe yaradığını.


GCC'nin 4 Aşaması

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.

Aşama 1 — Ön İşleme (Preprocessing)

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

Aşama 2 — Derleme (Compilation)

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

Aşama 3 — Birleştirme (Assembly)

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

Aşama 4 — Bağlama (Linking)

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.


Başlangıç Kodu: 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:

  1. Stack pointer'ı başlatır — SRAM'in en üstüne MSP (Main Stack Pointer) kurulur.
  2. Kesme vektör tablosunu kurar — Her kesmenin (interrupt) hangi fonksiyona gideceği belirlenir.
  3. Bellek bölümlerini başlatır — .data bölümündeki başlangıç değerleri Flash'tan RAM'e kopyalanır; .bss bölümü sıfırlanır.
  4. 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: Bellek Haritası

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:

AT> 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.


Firmware Build Akışı: Tümü Bir Arada

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.


Yararlı GCC / ARM Araç Zinciri Komutları

# 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.


Özet

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.

Kitap serisini incele →