Tüm eğitimler
TEKNİK REHBER GÖMÜLÜ LİNUX LINKER SCRIPTS 2026

Linker Scripts
GNU ld ile Bellek Haritası

Bare-metal ve gömülü sistemlerde bellek düzenini tam olarak kontrol et — ELF bölümleri, VMA/LMA ayrımı, startup kodu ve Cortex-M vector tablosu.

00 Linker nedir ve neden custom script gerekir

Linker (bağlayıcı), derleyicinin ürettiği nesne dosyalarını birleştirerek çalıştırılabilir bir binary oluşturur. Gömülü sistemlerde bu süreç, bellek haritasını tam olarak kontrol etmeyi zorunlu kılar.

Derleme zinciri: compiler → assembler → linker

Bir C kaynak dosyası çalıştırılabilir hale gelmeden önce birkaç aşamadan geçer. Ön işlemci (preprocessor) makro genişletme ve include işlemlerini yapar. Derleyici (compiler) C kodunu assembly diline çevirir. Birleştirici (assembler) assembly kodunu makine koduna — nesne dosyasına (.o) — dönüştürür. Son aşamada linker tüm nesne dosyalarını ve statik kütüphaneleri birleştirerek tek bir ELF binary'si üretir.

bash — derleme zinciri adımları
# Adım 1: Ön işleme (preprocessor)
arm-none-eabi-gcc -E main.c -o main.i

# Adım 2: Derleme (C → assembly)
arm-none-eabi-gcc -S main.c -o main.s

# Adım 3: Birleştirme (assembly → object file)
arm-none-eabi-as main.s -o main.o

# Adım 4: Bağlama (object files → ELF)
arm-none-eabi-ld -T memory.ld main.o startup.o -o firmware.elf

# Ya da tek komutla (gcc linker'ı çağırır)
arm-none-eabi-gcc -T memory.ld -nostdlib main.c startup.c -o firmware.elf

ELF output süreci

ELF (Executable and Linkable Format), modern Linux ve gömülü sistemlerin kullandığı standart binary formatıdır. ELF dosyası; ELF header (magic number, mimari, giriş noktası), program header tablosu (çalışma zamanı yükleme bilgisi) ve section header tablosu (sembol tablosu, debug bilgisi) içerir. Bağlama sürecinde linker, her nesne dosyasındaki bölümleri (.text, .data, .bss vb.) tek bir çıktı dosyasında birleştirir.

main.o (.text .data .bss) ──┐
startup.o (.text .vectors)  ├──► ld + memory.ld ──► firmware.elf ──► objcopy ──► firmware.bin
libc.a (.text .rodata)      ┘                                                    firmware.hex

Neden custom linker script gerekir

Masaüstü sistemlerde işletim sistemi binary'yi belleğe yerleştirir; linker script varsayılan olarak OS'un beklentilerine uygun üretilir. Gömülü ve bare-metal sistemlerde ise işletim sistemi yoktur. Hangi kodun nereye gideceğini, hangi verinin flash'ta kalıp hangisinin RAM'e kopyalanacağını, interrupt vektörlerinin nerede olduğunu, heap ve stack sınırlarını programcı belirlemelidir. Bu bilgileri linker'a aktarmanın tek yolu özel bir linker script yazmaktır.

Bellek haritası kontrolüFlash başlangıcı, RAM adresi, bootloader rezervasyonları — her SoC farklıdır
Çalışma zamanı başlatma.data bölümünü flash'tan RAM'e kopyalama, .bss'yi sıfırlama için semboller üretme
Kesme vektörleriARM Cortex-M'de vektör tablosu kesinlikle 0x08000000 adresinde başlamalı
Özel bölümlerBootloader, uygulama, firmware metadata — her biri ayrı adres aralığında
Dead code elimination--gc-sections ile kullanılmayan fonksiyonları at; küçük flash için kritik

Bu bölümde

  • Derleme zinciri: preprocessor → compiler → assembler → linker → ELF
  • Linker, nesne dosyalarını birleştirerek ELF binary üretir
  • Gömülü sistemlerde bellek haritası, vektör tablosu ve başlatma sembolleri için custom .ld şarttır
  • gcc, -T memory.ld bayrağıyla özel linker script kullanır

01 Minimal .ld yapısı: ENTRY, MEMORY, OUTPUT_FORMAT

Bir linker script'in üç temel bileşeni: giriş noktasını belirleyen ENTRY direktifi, fiziksel bellek bölgelerini tanımlayan MEMORY bloğu ve çıktı formatını belirleyen OUTPUT_FORMAT.

OUTPUT_FORMAT direktifi

Üretilecek ELF dosyasının byte sıralamasını (endianness) ve bit genişliğini belirler. ARM Cortex-M için little-endian 32-bit ELF kullanılır.

linker script — OUTPUT_FORMAT
/* Çıktı formatı: 32-bit little-endian ARM ELF */
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm")

/* Mimariyi açıkça belirt */
OUTPUT_ARCH(arm)

/* Alternatif: AArch64 için */
/* OUTPUT_FORMAT("elf64-littleaarch64") */
/* OUTPUT_ARCH(aarch64)                 */

ENTRY direktifi

Programın başladığı sembolü tanımlar. Bare-metal ARM'da bu genellikle startup kodundaki Reset_Handler veya _start fonksiyonudur. ELF header'daki entry point adresi bu sembolden alınır.

linker script — ENTRY
/* Giriş noktası: Reset_Handler sembolü startup.s içinde tanımlı */
ENTRY(Reset_Handler)

/* Alternatifler */
/* ENTRY(_start)     — Linux userspace geleneği */
/* ENTRY(main)       — çok basit bare-metal (önerilmez) */
/* ENTRY(0x08000000) — adres doğrudan (kaçınılmalı) */

MEMORY bloğu

Hedef sistemdeki fiziksel bellek bölgelerini tanımlar. Her bölüm bir isim, özellik listesi (attributes), başlangıç adresi (ORIGIN) ve uzunluktan (LENGTH) oluşur. Özellikler, hangi tür bölümlerin buraya yerleştirilebileceğini belirtir.

ÖzellikAnlamı
rOkunabilir (readable)
wYazılabilir (writable)
xÇalıştırılabilir (executable)
aTahsis edilebilir (allocatable)
i veya lBaşlatılmış bölüm
!Özelliği hariç tut
linker script — MEMORY bloğu (STM32F407 örneği)
MEMORY
{
  /* Flash: çalıştırılabilir, okunabilir, 1 MB */
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K

  /* SRAM: okuma/yazma, 128 KB */
  SRAM  (rwx) : ORIGIN = 0x20000000, LENGTH = 128K

  /* CCM RAM (Core Coupled Memory): sadece Cortex-M4'te, 64 KB */
  CCMRAM (rw) : ORIGIN = 0x10000000, LENGTH = 64K
}

/* Uzunluk birimleri: K (kilobyte), M (megabyte) veya ondalık bayt */
/* LENGTH = 1024K  ≡  LENGTH = 0x100000 */

Minimal eksiksiz linker script

minimal.ld — çalışan en küçük script
OUTPUT_FORMAT("elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(Reset_Handler)

MEMORY
{
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 256K
  SRAM  (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}

SECTIONS
{
  .text : { *(.text*) } > FLASH
  .data : { *(.data*) } > SRAM AT> FLASH
  .bss  : { *(.bss*)  } > SRAM
}
DIKKAT

ORIGIN ve LENGTH değerleri hedef SoC'nin datasheet'inden alınmalıdır. Yanlış adres, bootloader veya uygulama bölgelerinin üzerine yazılmasına neden olur. STM32'de varsayılan uygulama başlangıç adresi 0x08000000'dir; ancak bazı bootloader konfigürasyonlarında bu 0x08008000'e kaydırılabilir.

Bu bölümde

  • OUTPUT_FORMAT + OUTPUT_ARCH: ELF binary'nin byte sıralamasını ve mimarisini belirler
  • ENTRY(Reset_Handler): ELF giriş noktasını startup sembolüne bağlar
  • MEMORY bloğu: isim, özellik (rx/rw/rwx), ORIGIN ve LENGTH ile fiziksel belleği tanımlar
  • Özellikler linker'ın bölümleri doğru bellek bölgesine yerleştirmesini sağlar

02 SECTIONS bloğu: .text, .rodata, .data, .bss

SECTIONS bloğu, linker script'in kalbidir. Giriş bölümlerini (input sections) çıktı bölümlerine (output sections) haritar ve hangi bellek bölgesine gideceğini belirtir.

SECTIONS bloğu sözdizimi

Her çıktı bölümü bir isim, opsiyonel başlangıç adresi ve giriş bölüm kalıplarından oluşur. > operatörü VMA'yı (çalışma zamanı adresi), AT> operatörü ise LMA'yı (yükleme adresini) belirler.

linker script — SECTIONS genel yapısı
SECTIONS
{
  /* Çıktı bölüm adı : { giriş bölümleri } > VMA bölgesi [AT> LMA bölgesi] */
  output_section_name [address] : [AT(lma)]
  {
    /* Joker ile tüm .text bölümleri */
    *(.text .text.*)

    /* Belirli dosyadan bölüm */
    startup.o(.text)

    /* Hizalama */
    . = ALIGN(4);

    /* Sembol tanımlama */
    _end_of_text = .;
  } > MEMORY_REGION [AT> LMA_REGION]
}

.text bölümü (kod)

Çalıştırılabilir kod, flash'ta çalışma zamanında da okunabilir durumda kalır. Startup kodu (vektör tablosu ve Reset_Handler) .text'in başına yerleştirilmelidir.

linker script — .text bölümü
  .text :
  {
    /* Vektör tablosu (Cortex-M'de kesinlikle ilk)
       startup.o içindeki .isr_vector bölümü */
    KEEP(*(.isr_vector))

    /* Tüm nesne dosyalarındaki .text ve .text.* bölümleri */
    *(.text .text.*)

    /* ARM Thumb veneer (uzun atlama stub'ları) */
    *(.glue_7 .glue_7t)

    /* ARM exception handling tablosu */
    *(.ARM.extab* .ARM.exidx*)

    /* 4-byte hizalamaya getir */
    . = ALIGN(4);

    /* .text bölümünün sonu — startup kodunda kullanılır */
    _etext = .;
  } > FLASH

.rodata bölümü (salt-okunur veri)

String sabitleri, const dizileri, lookup tabloları flash'ta saklanır; XIP (Execute In Place) ile RAM'e kopyalanmadan doğrudan kullanılabilir.

linker script — .rodata bölümü
  .rodata :
  {
    *(.rodata .rodata.*)
    *(.rodata1)

    /* GCC'nin ürettiği salt-okunur veriler */
    *(.gnu.linkonce.r.*)

    . = ALIGN(4);
    _erodata = .;
  } > FLASH

.data bölümü (başlatılmış global değişkenler)

Başlangıç değeri olan global ve statik değişkenler flash'ta depolanır (LMA = FLASH), ancak çalışma zamanında RAM'de kullanılır (VMA = SRAM). Startup kodu bu kopyalamayı gerçekleştirir.

linker script — .data bölümü (LMA→VMA)
  /* .data: VMA = SRAM, LMA = FLASH (AT> ile) */
  .data :
  {
    /* .data başlangıç adresi (RAM'de) */
    _sdata = .;

    *(.data .data.*)
    *(.gnu.linkonce.d.*)

    . = ALIGN(4);

    /* .data bitiş adresi (RAM'de) */
    _edata = .;
  } > SRAM AT> FLASH
  /* SRAM: çalışma zamanı adresi (VMA)        */
  /* AT> FLASH: flash'taki yükleme adresi (LMA) */

  /* Flash'taki .data başlangıcı (LMA) */
  _sidata = LOADADDR(.data);

.bss bölümü (başlatılmamış global değişkenler)

.bss bölümü ELF dosyasında fiziksel alan kaplamaz (NOLOAD); yalnızca RAM'de ne kadar yer ayrılacağını belirtir. C standardına göre başlatılmamış global değişkenler sıfır olmalıdır; bu sıfırlama startup kodunda yapılır.

linker script — .bss bölümü
  /* .bss: NOLOAD — ELF dosyasında bayt kaplamaz */
  .bss (NOLOAD) :
  {
    /* .bss başlangıcı (startup'ta sıfırlanacak) */
    _sbss = .;

    *(.bss .bss.*)
    *(.gnu.linkonce.b.*)

    /* Başlatılmamış ama bellek tahsis eden değişkenler */
    *(COMMON)

    . = ALIGN(4);

    /* .bss sonu */
    _ebss = .;
  } > SRAM

ALIGN direktifi

Konumu belirtilen byte sınırına hizalar. ARM Cortex-M, 32-bit erişimler için 4-byte hizalaması gerektirir. Hizalanmamış erişimler Hard Fault oluşturur.

linker script — ALIGN kullanımı
  /* Konum sayacını 4-byte sınırına hizala */
  . = ALIGN(4);

  /* Konum sayacını 8-byte sınırına hizala (64-bit veri için) */
  . = ALIGN(8);

  /* Konum sayacını 256-byte sınırına hizala (Cortex-M NVIC tablosu) */
  . = ALIGN(256);

  /* Bölüm içinde ALIGN fonksiyonu */
  .text :
  {
    *(.text*)
    . = ALIGN(4);   /* .text sonunda hizala */
  } > FLASH

Bu bölümde

  • .text: çalıştırılabilir kod → FLASH; vektör tablosu en başa KEEP ile sabitlenir
  • .rodata: salt-okunur sabitler → FLASH; XIP ile RAM kopyası gerekmez
  • .data: başlatılmış değişkenler → VMA=SRAM, LMA=FLASH; startup kopyalar
  • .bss: başlatılmamış değişkenler → SRAM; NOLOAD ile ELF'de yer kaplamaz, startup sıfırlar

03 VMA vs LMA: flash'tan RAM'e kopyalama

VMA (Virtual Memory Address) çalışma zamanı adresi, LMA (Load Memory Address) ise verinin depolandığı fiziksel adrestir. Bu ayrım, flash'taki başlangıç değerlerini RAM'e kopyalamak için kritiktir.

VMA ve LMA kavramları

Bir ELF bölümünün iki farklı adresi olabilir. LMA, verinin fiziksel olarak depolandığı adrestir — genellikle flash. VMA, programın çalışma sırasında bu veriye hangi adresle erişeceğidir — genellikle RAM. Kod bölümleri (.text) için VMA = LMA'dır çünkü doğrudan flash'tan çalıştırılır (XIP). Veri bölümleri (.data) için ise LMA flash'ta, VMA RAM'dedir.

BölümVMALMAAçıklama
.textFLASHFLASHFlash'tan doğrudan çalışır (XIP)
.rodataFLASHFLASHFlash'tan doğrudan okunur
.dataSRAMFLASHFlash'ta depolanır, startup RAM'e kopyalar
.bssSRAMELF'de yok; startup sıfırlar

XIP (Execute In Place)

Bazı sistemlerde, özellikle NOR flash kullananllarda, kod doğrudan flash adres alanından çalıştırılabilir. Bu durumda kod için RAM kopyası gerekmez. Ancak NOR flash, DRAM'den çok daha yavaştır; performans kritik rutinleri (interrupt handler'lar, DMA callback'leri) RAM'e kopyalamak gerekebilir. Bu amaçla __attribute__((section(".ramfunc"))) kullanılır.

linker script — .ramfunc bölümü (RAM'de çalışan kod)
  /* .ramfunc: VMA = SRAM, LMA = FLASH → startup kopyalar */
  .ramfunc :
  {
    . = ALIGN(4);
    _sramfunc = .;
    *(.ramfunc .ramfunc.*)
    . = ALIGN(4);
    _eramfunc = .;
  } > SRAM AT> FLASH

  _siramfunc = LOADADDR(.ramfunc);
C — RAM'e taşınacak fonksiyon
/* Bu fonksiyon RAM'de çalışır (flash erase sırasında güvenli) */
__attribute__((section(".ramfunc")))
void flash_erase_sector(uint32_t sector)
{
    /* Flash yazma/silme kodu buraya */
}

Kritik linker sembolleri

Startup kodu .data kopyalaması ve .bss sıfırlaması için belirli semboller kullanır. Bu semboller linker script'te tanımlanır ve C kodunda extern olarak erişilir.

SembolTanımKullanım
_etext.text + .rodata sonu (FLASH'ta).data'nın flash'taki başlangıcını bulmak için
_sidata.data bölümünün LMA'sı (FLASH)Kopyalama kaynağı
_sdata.data bölümünün VMA başlangıcı (SRAM)Kopyalama hedefi başlangıcı
_edata.data bölümünün VMA sonu (SRAM)Kopyalama döngüsü bitiş koşulu
_sbss.bss başlangıcı (SRAM)Sıfırlama başlangıcı
_ebss.bss sonu (SRAM)Sıfırlama bitiş koşulu
linker script — sembol tanımlamaları
  .text :
  {
    *(.text*)
    . = ALIGN(4);
    _etext = .;     /* .text sonu */
  } > FLASH

  .rodata :
  {
    *(.rodata*)
    . = ALIGN(4);
  } > FLASH

  /* .data'nın flash'taki LMA adresi */
  _sidata = LOADADDR(.data);

  .data :
  {
    _sdata = .;     /* .data VMA başlangıcı (SRAM) */
    *(.data*)
    . = ALIGN(4);
    _edata = .;     /* .data VMA sonu (SRAM) */
  } > SRAM AT> FLASH

  .bss (NOLOAD) :
  {
    _sbss = .;      /* .bss başlangıcı */
    *(.bss*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;      /* .bss sonu */
  } > SRAM

Bu bölümde

  • LMA = yükleme adresi (flash'ta depolama); VMA = çalışma zamanı adresi (RAM'de kullanım)
  • .text ve .rodata: VMA = LMA = FLASH; XIP ile doğrudan çalışır
  • .data: LMA = FLASH, VMA = SRAM; AT> FLASH ve LOADADDR(.data) ile kopyalama adresi bulunur
  • _sidata, _sdata, _edata, _sbss, _ebss: startup kodunun başlatma işlemleri için zorunlu semboller

04 Startup kodu: crt0 ve C'ye atlama

Startup kodu, sıfırlamanın (reset) ardından C çalışma ortamını hazırlar: .data kopyalama, .bss sıfırlama, FPU etkinleştirme ve son olarak main()'e atlama.

Startup kodunun görevi

C dili, global değişkenlerin başlangıç değerleriyle yüklendiğini ve başlatılmamış değişkenlerin sıfır olduğunu garanti eder. İşletim sistemli sistemlerde bu ELF yükleyici tarafından yapılır. Bare-metal sistemlerde bu işi startup kodu üstlenir. Startup kodu assembly (crt0.s) veya C ile yazılabilir.

Assembly startup (crt0.s)

startup.s — ARM Thumb assembly startup
    .syntax unified
    .cpu    cortex-m4
    .thumb

    .section .isr_vector, "a", %progbits
    .type   g_pfnVectors, %object

g_pfnVectors:
    .word   _estack                 /* İlk 4 bayt: stack pointer başlangıcı */
    .word   Reset_Handler           /* Reset vektörü */
    .word   NMI_Handler             /* NMI */
    .word   HardFault_Handler       /* Hard Fault */
    .word   MemManage_Handler       /* MPU Fault */
    .word   BusFault_Handler        /* Bus Fault */
    .word   UsageFault_Handler      /* Usage Fault */
    /* ... diğer vektörler ... */

    .section .text.Reset_Handler
    .type   Reset_Handler, %function
Reset_Handler:
    /* Stack pointer zaten vektör tablosundan yüklendi */

    /* .data kopyalama: FLASH → SRAM */
    ldr     r0, =_sdata             /* Hedef: SRAM başlangıcı */
    ldr     r1, =_edata             /* Hedef: SRAM sonu */
    ldr     r2, =_sidata            /* Kaynak: FLASH'taki LMA */
    movs    r3, #0
    b       LoopCopyDataInit

CopyDataInit:
    ldr     r4, [r2, r3]            /* FLASH'tan oku */
    str     r4, [r0, r3]            /* SRAM'a yaz */
    adds    r3, r3, #4

LoopCopyDataInit:
    adds    r4, r0, r3
    cmp     r4, r1
    bcc     CopyDataInit            /* _edata'ya ulaşana kadar döngü */

    /* .bss sıfırlama */
    ldr     r2, =_sbss
    ldr     r4, =_ebss
    movs    r3, #0
    b       LoopFillZerobss

FillZerobss:
    str     r3, [r2]
    adds    r2, r2, #4

LoopFillZerobss:
    cmp     r2, r4
    bcc     FillZerobss             /* _ebss'e ulaşana kadar sıfırla */

    /* main() çağır — dönmemeli */
    bl      main

    /* main dönerse sonsuz döngü */
LoopForever:
    b       LoopForever

    .size   Reset_Handler, .-Reset_Handler

C ile startup kodu

Assembly yerine C ile de startup yazılabilir; bu daha okunabilir ancak derleyicinin bazı optimizasyonları (örn. stack kullanımı) sorun çıkarabilir. __attribute__((naked)) veya dikkatli derleme bayrakları gerekir.

startup.c — C startup kodu
#include <stdint.h>
#include <string.h>

/* Linker script semboller */
extern uint32_t _sidata;   /* .data LMA (FLASH) */
extern uint32_t _sdata;    /* .data VMA başlangıcı (SRAM) */
extern uint32_t _edata;    /* .data VMA sonu (SRAM) */
extern uint32_t _sbss;     /* .bss başlangıcı */
extern uint32_t _ebss;     /* .bss sonu */

/* main() prototipi */
extern int main(void);

void Reset_Handler(void)
{
    uint32_t *src, *dst;

    /* .data kopyalama: FLASH → SRAM */
    src = &_sidata;   /* LMA: flash'taki başlangıç */
    dst = &_sdata;    /* VMA: SRAM'daki hedef */
    while (dst < &_edata) {
        *dst++ = *src++;
    }

    /* .bss sıfırlama */
    dst = &_sbss;
    while (dst < &_ebss) {
        *dst++ = 0;
    }

    /* C++ global constructor'ları çağır (varsa) */
    /* __libc_init_array(); */

    /* main'e atla */
    main();

    /* main dönerse sonsuz döngü */
    while (1) { }
}
NOT

Startup kodunu C ile yazarken -fno-stack-protector, -nostartfiles ve -nostdlib bayraklarını kullanın. Derleyici stack cookie kurulumu için libc'ye başvurabilir; bu bare-metal'da bağlama hatasına neden olur.

Bu bölümde

  • Startup kodu: reset'ten C'ye geçişi sağlar; .data kopyalama + .bss sıfırlama + main() çağrısı
  • Assembly startup: doğrudan register kontrol; C startup: daha okunabilir ama dikkat gerektirir
  • Linker sembolleri (&_sidata, &_sdata, &_edata, &_sbss, &_ebss) C'de extern uint32_t olarak tanımlanır
  • main() dönerse sonsuz döngü — bare-metal'da OS yoktur, geri dönecek yer yok

05 Heap ve stack tanımlama

Linker script, heap ve stack boyutlarını ve konumlarını belirler. Bu semboller çalışma zamanı kütüphaneleri (newlib, picolibc) ve uygulama kodu tarafından kullanılır.

Stack tanımı

ARM Cortex-M, reset sonrası stack pointer'ı vektör tablosunun ilk word'ünden yükler. Linker script bu değeri _estack sembolü aracılığıyla sağlar. Stack SRAM'ın en üstünde, aşağıya doğru büyür (full-descending stack).

linker script — stack tanımı
MEMORY
{
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K
  SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

/* Stack boyutu: 8 KB */
_Min_Stack_Size = 0x2000;

SECTIONS
{
  /* ... diğer bölümler ... */

  /* Stack SRAM'ın en sonunda */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE ( end = . );
    PROVIDE ( _end = . );
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } > SRAM

  /* SRAM'ın en üstü = stack pointer başlangıcı */
  _estack = ORIGIN(SRAM) + LENGTH(SRAM);
}

Heap tanımı

Heap, dinamik bellek tahsisi (malloc/free) için kullanılır. .bss bitişinde başlar ve stack'e kadar büyür. newlib'in _sbrk() sistem çağrısı heap'i büyütmek için bu sınırları kullanır.

linker script — heap tanımı
/* Minimum heap boyutu: 4 KB */
_Min_Heap_Size = 0x1000;

SECTIONS
{
  .bss (NOLOAD) :
  {
    _sbss = .;
    *(.bss*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
  } > SRAM

  /* Heap: .bss bitişinde başlar */
  .heap (NOLOAD) :
  {
    . = ALIGN(8);
    __heap_start = .;
    . = . + _Min_Heap_Size;
    __heap_end = .;
    . = ALIGN(8);
  } > SRAM

  /* Stack: SRAM'ın en üstünde */
  .stack (NOLOAD) :
  {
    . = ALIGN(8);
    __stack_limit = .;
    . = . + _Min_Stack_Size;
    __stack_top = .;
    . = ALIGN(8);
  } > SRAM

  _estack = __stack_top;
}

Linker sembolünü C'den okuma

Linker script'te tanımlanan semboller, C kodunda özel sözdizimi ile erişilir. Sembol bir adres değil, o adresin kendisidir; bu nedenle & operatörü ile adresi alınır.

C — linker sembollerine erişim
#include <stdint.h>
#include <stddef.h>

/* Linker sembollerini extern olarak tanımla */
extern uint32_t __heap_start;
extern uint32_t __heap_end;
extern uint32_t __stack_top;
extern uint32_t __stack_limit;

/* Heap boyutunu hesapla */
size_t get_heap_size(void)
{
    return (size_t)(&__heap_end - &__heap_start) * sizeof(uint32_t);
}

/* newlib için _sbrk implementasyonu */
void *_sbrk(ptrdiff_t incr)
{
    static uint8_t *heap_end = (uint8_t *)&__heap_start;
    uint8_t *prev_heap_end = heap_end;

    if (heap_end + incr > (uint8_t *)&__heap_end) {
        /* Heap taşması */
        return (void *)-1;
    }

    heap_end += incr;
    return (void *)prev_heap_end;
}

/* Stack'in ne kadar kullanıldığını ölçme */
uint32_t measure_stack_usage(void)
{
    register uint32_t sp __asm__("sp");
    return (uint32_t)&__stack_top - sp;
}
DIKKAT

Stack ve heap çakışması (stack overflow) bare-metal sistemlerde en yaygın hatalardan biridir. Güvenli tasarım için __stack_limit adresine MPU (Memory Protection Unit) ile koruma ekleyin. Stack usage watermark yöntemiyle maksimum kullanımı ölçün: startup'ta __stack_limit'ten __stack_top'a kadar belirli bir desenle (0xDEADBEEF) doldurun, sonra ne kadarının değiştiğini kontrol edin.

Bu bölümde

  • Stack: SRAM'ın en üstünde, _estack sembolü vektör tablosuna ilk word olarak yazılır
  • Heap: .bss bitişinde, __heap_start/__heap_end ile sınırlandırılır
  • Linker sembollerine C'de extern uint32_t __sembol; + &__sembol ile erişilir
  • newlib _sbrk(): heap_end pointer'ı __heap_start'tan __heap_end'e kadar yönetir

06 Overlay, PROVIDE, KEEP ve --gc-sections

İleri düzey linker script teknikleri: bellek bankası değiştirme için overlay, güvenli sembol tanımı için PROVIDE, dead code elimination için --gc-sections ve KEEP direktifi.

PROVIDE direktifi

PROVIDE, sadece başka hiçbir nesne dosyasında tanımlanmamışsa sembolü tanımlar. Varsayılan değer oluşturmak için idealdir; kullanıcı sembolü tanımlarsa PROVIDE'ın değeri görmezden gelinir.

linker script — PROVIDE
/* Eğer _sbrk hiçbir yerde tanımlı değilse bu adresi kullan */
PROVIDE(_sbrk = 0x20001000);

/* Varsayılan IRQ handler — kullanıcı tanımlarsa override edilir */
PROVIDE(Default_Handler = _default_handler_impl);

/* PROVIDE_HIDDEN: tanımı gizle (dışa aktarma) */
PROVIDE_HIDDEN(__bss_start__ = _sbss);
PROVIDE_HIDDEN(__bss_end__   = _ebss);

KEEP direktifi

--gc-sections etkinleştirildiğinde linker, referans edilmeyen bölümleri atar. KEEP, belirli bölümlerin her zaman çıktıya dahil edilmesini zorlar. Vektör tablosu, bootloader metadata ve linker sembol tanımlayan bölümler için zorunludur.

linker script — KEEP kullanımı
  .isr_vector :
  {
    /* Vektör tablosu: --gc-sections tarafından silinmesin */
    KEEP(*(.isr_vector))
    KEEP(*(.vectors))
  } > FLASH

  /* ARM exception unwind tablosu */
  .ARM.exidx :
  {
    __exidx_start = .;
    KEEP(*(.ARM.exidx*))
    __exidx_end = .;
  } > FLASH

  /* Firmware versiyon bilgisi — objcopy ile okunacak */
  .fw_info :
  {
    KEEP(*(.fw_version))
    KEEP(*(.fw_build_date))
  } > FLASH

--gc-sections ile dead code elimination

Kullanılmayan fonksiyonlar ve veri, derleyicinin -ffunction-sections ve -fdata-sections bayraklarıyla ayrı bölümlere konulur. Linker --gc-sections ile bu bölümleri atar. Küçük flash'ta büyük boyut tasarrufu sağlar.

bash — gc-sections kullanımı
# Her fonksiyon/değişken için ayrı bölüm üret
arm-none-eabi-gcc \
  -ffunction-sections \
  -fdata-sections \
  -c main.c -o main.o

# Kullanılmayan bölümleri at
arm-none-eabi-gcc \
  -T memory.ld \
  -Wl,--gc-sections \
  -Wl,-Map=firmware.map \
  main.o startup.o -o firmware.elf

# Atılan bölümleri görmek için
arm-none-eabi-gcc \
  -Wl,--gc-sections,--print-gc-sections \
  -T memory.ld main.o startup.o -o firmware.elf 2>&1 | head -20

OVERLAY: bellek bankası değiştirme

Overlay, aynı bellek alanını farklı zamanlarda farklı kod/veri için kullanmayı sağlar. Çok az RAM olan sistemlerde birden fazla büyük kod modülü aynı RAM alanını paylaşır; aktif olmayan modül flash'ta bekler.

linker script — OVERLAY yapısı
/* İki overlay bölümü aynı RAM alanını paylaşır */
OVERLAY 0x20002000 : AT(0x08040000)
{
  .module_a { module_a.o(.text*) }
  .module_b { module_b.o(.text*) }
}

/* OVERLAY, her bölüm için otomatik semboller üretir:
   __load_start_module_a, __load_stop_module_a
   __load_start_module_b, __load_stop_module_b */

DISCARD bölümü

Belirli bölümleri çıktıdan tamamen çıkarmak için /DISCARD/ kullanılır. Debug bilgisi veya Linux-spesifik metadata bare-metal binary'de gerekmez.

linker script — DISCARD
  /DISCARD/ :
  {
    /* C++ exception handling — bare-metal'da gereksiz */
    *(.eh_frame)
    *(.eh_frame_hdr)
    *(.note.gnu.build-id)

    /* Linux-spesifik not bölümleri */
    *(.note*)
  }

Bu bölümde

  • PROVIDE: sembol yalnızca başka yerde tanımlı değilse oluşturulur; varsayılan değer için idealdir
  • KEEP: --gc-sections'dan korunan bölümler; vektör tablosu ve metadata için zorunlu
  • -ffunction-sections -fdata-sections + --gc-sections: kullanılmayan kodu atar, flash tasarrufu sağlar
  • OVERLAY: aynı RAM alanını birden fazla modül arasında paylaştırır

07 Bare-metal ARM Cortex-M örneği

STM32F407 için tam bir linker script: vektör tablosu, Thumb bit, scatter loading ve Cortex-M'e özgü bölümler.

Cortex-M bellek haritası

ARM Cortex-M mimarisi sabit bir bellek haritası tanımlar. Kod genellikle 0x00000000-0x1FFFFFFF aralığında, SRAM 0x20000000'da başlar. STM32'de flash 0x08000000'a yansıtılır; reset sonrası bu adres 0x00000000'dan da erişilebilir.

Adres AralığıBölgeSTM32F407
0x08000000–0x080FFFFFFlash (1 MB)Kod + sabitler
0x10000000–0x1000FFFFCCM RAM (64 KB)Hızlı veri
0x20000000–0x2001FFFFSRAM1+2 (128 KB)Veri + heap + stack
0x40000000–0x5FFFFFFFPeripheralAPB1/2, AHB1/2
0xE0000000–0xE00FFFFFPrivate Peripheral BusNVIC, SysTick, DWT

Tam STM32F407 linker script

stm32f407.ld — tam linker script
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(Reset_Handler)

_Min_Heap_Size  = 0x2000;  /* 8 KB minimum heap */
_Min_Stack_Size = 0x4000;  /* 16 KB minimum stack */

MEMORY
{
  FLASH   (rx)  : ORIGIN = 0x08000000, LENGTH = 1024K
  CCMRAM  (rw)  : ORIGIN = 0x10000000, LENGTH = 64K
  SRAM    (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
  /* Vektör tablosu: FLASH'ın en başında */
  .isr_vector :
  {
    . = ALIGN(4);
    KEEP(*(.isr_vector))
    . = ALIGN(4);
  } > FLASH

  /* Kod bölümü */
  .text :
  {
    . = ALIGN(4);
    *(.text .text.*)
    *(.glue_7)
    *(.glue_7t)
    *(.eh_frame)
    KEEP(*(.init))
    KEEP(*(.fini))
    . = ALIGN(4);
    _etext = .;
  } > FLASH

  /* ARM exception unwind tablosu */
  .ARM.extab :
  {
    *(.ARM.extab* .gnu.linkonce.armextab.*)
  } > FLASH

  .ARM :
  {
    __exidx_start = .;
    *(.ARM.exidx*)
    __exidx_end = .;
  } > FLASH

  /* C/C++ init/fini dizileri */
  .preinit_array :
  {
    PROVIDE_HIDDEN(__preinit_array_start = .);
    KEEP(*(.preinit_array*))
    PROVIDE_HIDDEN(__preinit_array_end = .);
  } > FLASH

  .init_array :
  {
    PROVIDE_HIDDEN(__init_array_start = .);
    KEEP(*(SORT(.init_array.*)))
    KEEP(*(.init_array*))
    PROVIDE_HIDDEN(__init_array_end = .);
  } > FLASH

  .fini_array :
  {
    PROVIDE_HIDDEN(__fini_array_start = .);
    KEEP(*(SORT(.fini_array.*)))
    KEEP(*(.fini_array*))
    PROVIDE_HIDDEN(__fini_array_end = .);
  } > FLASH

  /* Salt-okunur veri */
  .rodata :
  {
    . = ALIGN(4);
    *(.rodata .rodata.*)
    *(.gnu.linkonce.r.*)
    . = ALIGN(4);
  } > FLASH

  /* .data LMA adresi (flash'ta) */
  _sidata = LOADADDR(.data);

  /* Başlatılmış veri: VMA=SRAM, LMA=FLASH */
  .data :
  {
    . = ALIGN(4);
    _sdata = .;
    *(.data .data.*)
    *(.gnu.linkonce.d.*)
    . = ALIGN(4);
    _edata = .;
  } > SRAM AT> FLASH

  /* CCM RAM'e gidecek veri */
  _siccmram = LOADADDR(.ccmram);

  .ccmram :
  {
    . = ALIGN(4);
    _sccmram = .;
    *(.ccmram .ccmram.*)
    . = ALIGN(4);
    _eccmram = .;
  } > CCMRAM AT> FLASH

  /* Başlatılmamış veri */
  .bss (NOLOAD) :
  {
    . = ALIGN(4);
    _sbss = .;
    *(.bss .bss.*)
    *(.gnu.linkonce.b.*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
  } > SRAM

  /* Heap ve stack */
  ._user_heap_stack :
  {
    . = ALIGN(8);
    PROVIDE(end = .);
    PROVIDE(_end = .);
    . = . + _Min_Heap_Size;
    . = . + _Min_Stack_Size;
    . = ALIGN(8);
  } > SRAM

  _estack = ORIGIN(SRAM) + LENGTH(SRAM);

  /* Debug bölümleri — binary boyutunu artırmaz */
  .debug_info     0 : { *(.debug_info) }
  .debug_abbrev   0 : { *(.debug_abbrev) }
  .debug_loc      0 : { *(.debug_loc) }
  .debug_ranges   0 : { *(.debug_ranges) }
  .debug_line     0 : { *(.debug_line) }
  .debug_str      0 : { *(.debug_str) }
}

Thumb bit ve vektör tablosu

ARM Cortex-M yalnızca Thumb-2 komut setini destekler. Vektör tablosundaki fonksiyon adresleri LSB=1 (Thumb bit) olmalıdır. GNU assembler bu biti otomatik ayarlar; ancak linker script veya C kodu ile adres yazarken dikkat edilmelidir.

startup.c — Cortex-M vektör tablosu (C)
#include <stdint.h>

extern uint32_t _estack;
extern void Reset_Handler(void);
extern void NMI_Handler(void);
extern void HardFault_Handler(void);

/* Varsayılan IRQ handler */
void Default_Handler(void) { while(1); }

/* Zayıf alias: kullanıcı tanımlamazsa Default_Handler kullanılır */
void SysTick_Handler(void)  __attribute__((weak, alias("Default_Handler")));
void USART1_IRQHandler(void) __attribute__((weak, alias("Default_Handler")));
void TIM2_IRQHandler(void)  __attribute__((weak, alias("Default_Handler")));

/* Vektör tablosu: .isr_vector bölümüne yerleştirilir */
__attribute__((section(".isr_vector")))
const uint32_t g_pfnVectors[] = {
    (uint32_t)&_estack,          /* 0x000: Stack pointer */
    (uint32_t)Reset_Handler,     /* 0x004: Reset */
    (uint32_t)NMI_Handler,       /* 0x008: NMI */
    (uint32_t)HardFault_Handler, /* 0x00C: Hard Fault */
    /* ... 240+ IRQ vektörü ... */
    (uint32_t)SysTick_Handler,   /* 0x03C: SysTick */
    (uint32_t)USART1_IRQHandler, /* IRQ37: USART1 */
    (uint32_t)TIM2_IRQHandler,   /* IRQ28: TIM2 */
};

Bu bölümde

  • STM32F407: FLASH=0x08000000/1MB, SRAM=0x20000000/128KB, CCM=0x10000000/64KB
  • Vektör tablosu: FLASH'ın ilk word'ü _estack, ikincisi Reset_Handler adresi
  • Thumb bit: GNU toolchain vektör adresleri için LSB=1'i otomatik ayarlar
  • CCM RAM: AT> FLASH ile scatter loading; yüksek hızlı ISR veri için kullanılır

08 Pratik: custom .ld ile hello world, objdump ve map dosyası

Sıfırdan linker script yazıp QEMU'da çalıştırma, objdump ile bölüm kontrolü, size komutu ile bellek kullanımı ve map dosyası okuma.

Minimal bare-metal hello world

main.c — bare-metal hello (UART)
#include <stdint.h>

/* QEMU versatile PB UART0 adresi */
#define UART0_BASE  0x101F1000
#define UART0_DR    (*(volatile uint32_t *)(UART0_BASE + 0x00))
#define UART0_FR    (*(volatile uint32_t *)(UART0_BASE + 0x18))
#define UART_FR_TXFF (1 << 5)

/* Başlatılmış global değişken (.data'da) */
static int boot_count = 1;

/* Başlatılmamış global (.bss'de) */
static char message[64];

void uart_putc(char c)
{
    while (UART0_FR & UART_FR_TXFF) { }
    UART0_DR = (uint32_t)c;
}

void uart_puts(const char *s)
{
    while (*s) uart_putc(*s++);
}

int main(void)
{
    uart_puts("Hello from bare-metal!\r\n");
    uart_puts("Boot count: ");
    uart_putc('0' + boot_count);
    uart_puts("\r\n");
    while (1) { }
    return 0;
}
versatile.ld — QEMU versatile PB linker script
OUTPUT_FORMAT("elf32-littlearm")
OUTPUT_ARCH(arm)
ENTRY(_start)

MEMORY
{
  /* QEMU versatile PB: RAM 0x10000, ROM 0x0 */
  ROM (rx)  : ORIGIN = 0x00010000, LENGTH = 256K
  RAM (rwx) : ORIGIN = 0x00090000, LENGTH = 512K
}

SECTIONS
{
  .text :
  {
    _start = .;
    *(.text.startup)
    *(.text*)
    *(.rodata*)
    . = ALIGN(4);
    _etext = .;
  } > ROM

  _sidata = LOADADDR(.data);

  .data :
  {
    . = ALIGN(4);
    _sdata = .;
    *(.data*)
    . = ALIGN(4);
    _edata = .;
  } > RAM AT> ROM

  .bss (NOLOAD) :
  {
    . = ALIGN(4);
    _sbss = .;
    *(.bss*)
    *(COMMON)
    . = ALIGN(4);
    _ebss = .;
  } > RAM

  _estack = ORIGIN(RAM) + LENGTH(RAM);
}
bash — derleme ve QEMU'da çalıştırma
# Derleme
arm-none-eabi-gcc \
  -mcpu=arm926ej-s \
  -T versatile.ld \
  -nostdlib \
  -ffunction-sections \
  -fdata-sections \
  -Wl,--gc-sections \
  -Wl,-Map=firmware.map \
  startup.c main.c -o firmware.elf

# ELF → binary
arm-none-eabi-objcopy -O binary firmware.elf firmware.bin

# QEMU'da çalıştır
qemu-system-arm \
  -M versatilepb \
  -kernel firmware.elf \
  -nographic \
  -serial stdio

objdump ile bölüm kontrolü

bash — objdump ve size
# Bölüm başlıklarını listele
arm-none-eabi-objdump -h firmware.elf
# Sections:
# Idx Name      Size      VMA       LMA       File off  Algn
#   0 .text     00001234  00010000  00010000  00008000  2**2
#   1 .rodata   00000080  00011234  00011234  00009234  2**2
#   2 .data     0000001c  00090000  000112b4  00009380  2**2
#   3 .bss      00000040  00090020  00090020  0000939c  2**2

# Bellek kullanımı (text=kod, data=başlatılmış, bss=sıfırlanan)
arm-none-eabi-size firmware.elf
#    text    data     bss     dec     hex filename
#    4660      28      64    4752    1290 firmware.elf

# Assembly çıktısı (disassemble)
arm-none-eabi-objdump -d firmware.elf | head -40

# Sembol tablosu
arm-none-eabi-nm --print-size --size-sort firmware.elf

Map dosyası okuma

Map dosyası, linker'ın tam yerleşim kararlarını gösterir: hangi nesne dosyasından hangi bölümün nereye gittiği, her sembolün adresi ve boyutu.

firmware.map — örnek çıktı
# Map dosyasının önemli bölümleri:

# Bellek haritası özeti
Memory Configuration

Name             Origin             Length             Attributes
ROM              0x0000000000010000 0x0000000000040000 xr
RAM              0x0000000000090000 0x0000000000080000 xrw

# Bölüm yerleşimi
Linker script and memory map

.text           0x0000000000010000     0x1234
 *(.text.startup)
 .text.startup  0x0000000000010000       0x80 startup.o
 *(.text*)
 .text.main     0x0000000000010080      0x120 main.o
 .text.uart_puts 0x00000000000101a0       0x40 main.o

.data           0x0000000000090000       0x1c
 _sdata = .
 .data.boot_count 0x0000000000090000        0x4 main.o
 _edata = .

.bss            0x0000000000090020       0x40
 _sbss = .
 .bss.message   0x0000000000090020       0x40 main.o
 _ebss = .

Bu bölümde

  • QEMU versatile PB: ROM=0x10000, RAM=0x90000; kernel olarak ELF doğrudan yüklenebilir
  • objdump -h: VMA, LMA, boyut ve dosya ofseti ile tüm bölümleri gösterir
  • arm-none-eabi-size: text/data/bss boyutlarını özetler; flash ve RAM kullanımını hızlı kontrol
  • Map dosyası (-Map=firmware.map): hangi nesne dosyasından hangi sembolün nereye yerleştiğini tam olarak gösterir