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.
# 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.
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.ldbayrağı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.
/* Çı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.
/* 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.
| Özellik | Anlamı |
|---|---|
r | Okunabilir (readable) |
w | Yazılabilir (writable) |
x | Çalıştırılabilir (executable) |
a | Tahsis edilebilir (allocatable) |
i veya l | Başlatılmış bölüm |
! | Özelliği hariç tut |
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
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
}
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.
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.
.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.
.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.
/* .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.
/* .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.
/* 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üm | VMA | LMA | Açıklama |
|---|---|---|---|
| .text | FLASH | FLASH | Flash'tan doğrudan çalışır (XIP) |
| .rodata | FLASH | FLASH | Flash'tan doğrudan okunur |
| .data | SRAM | FLASH | Flash'ta depolanır, startup RAM'e kopyalar |
| .bss | SRAM | — | ELF'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.
/* .ramfunc: VMA = SRAM, LMA = FLASH → startup kopyalar */
.ramfunc :
{
. = ALIGN(4);
_sramfunc = .;
*(.ramfunc .ramfunc.*)
. = ALIGN(4);
_eramfunc = .;
} > SRAM AT> FLASH
_siramfunc = LOADADDR(.ramfunc);
/* 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.
| Sembol | Tanım | Kullanı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 |
.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> FLASHveLOADADDR(.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)
.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.
#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) { }
}
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).
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.
/* 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.
#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;
}
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,
_estacksembolü vektör tablosuna ilk word olarak yazılır - Heap: .bss bitişinde,
__heap_start/__heap_endile sınırlandırılır - Linker sembollerine C'de
extern uint32_t __sembol;+&__sembolile 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.
/* 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.
.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.
# 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.
/* İ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.
/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ölge | STM32F407 |
|---|---|---|
| 0x08000000–0x080FFFFF | Flash (1 MB) | Kod + sabitler |
| 0x10000000–0x1000FFFF | CCM RAM (64 KB) | Hızlı veri |
| 0x20000000–0x2001FFFF | SRAM1+2 (128 KB) | Veri + heap + stack |
| 0x40000000–0x5FFFFFFF | Peripheral | APB1/2, AHB1/2 |
| 0xE0000000–0xE00FFFFF | Private Peripheral Bus | NVIC, SysTick, DWT |
Tam STM32F407 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.
#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
#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;
}
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);
}
# 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ü
# 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.
# 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