2022年12月2日 星期五

作業系統之前的程式 for stm32f4discovery (19) - 遲來的 linker script

雄辯是銀, 沉默是金
這個系列的最後一篇是 2016, 過去 6 年了, 很可惜我還是沒寫出一個 os for stm32f4discovery, 有些忘記 linker script 的用法了, 用這篇文章來介紹 linker script, 當然, 只是入門等級的介紹。這個應該要早點介紹的, 不過怎麼開發 stm32f4 比較要緊, linker script 就這麼被耽擱了。

找資料期間發現一篇寫的很不錯的文章「linker script 簡單教學」, 真的很高興不用總是讀那些簡體中文文章, 不要誤會, 簡體中文文章本身沒問題, 但我總是不習慣中國人的用語, 難得可以看到本土的技術文章。

linker script 最主要是要讓執行檔長成可以執行的樣子, 這是什麼意思呢? 就是字面上的意思。每個平台都不同, 例如 cortex-m 前 4 個 byte 是 stack address, 所以就必須想辦法讓執行檔的前 4 byte 指向設定好的 stack 區域, 那你說不是應該要指定 sp 暫存器來設定嗎? 一般來說是這樣, 不過 cortex-m 不需要使用組合語言就可以設定 sp 暫存器。可以參考: 作業系統之前的程式 for stm32f4 - discovery (1) - 1 加到 10 , asm version

因為這是 bare-metal 程式, 所以才會有這樣的需求, 如果是 linux/windows 執行檔, 就不需要這麼嚴格讓執行檔長成某個樣子。

當然, 我自己是寫不出來這個 linker script, 這是從某本書上抄來的, 一般開發商付的 sdk 也會有一個 linker script。

最容易讓我困擾的是 VMA/LMA, 老是記不住誰是誰? 可以使用以下指令觀察 VMA/LMA arm-none-eabi-objdump -h mygpio_led.elf, 這個指令很有幫助, 讓我可以區分 VMA/LMA, 使用 readelf 我不知道怎麼讀出這個資訊。

mygpio_led.c
 1 #define USE_STDPERIPH_DRIVER
 2 #include "stm32.h"
 3 
 4 int val=0x123456ab;
 5 
 6 void Delay(uint32_t delay )
 7 {
 8   int d1=5;
 9   while(d1) delay--;
10 }
11 
12 
13 int main(void)
14 {
15   Delay(0x3FFFFF);
16 }

list 1. stm32.ld
 1 /*
 2 MEMORY
 3 {
 4   FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 128K
 5   SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
 6 }
 7 */
 8 
 9 /* Specify the memory areas */
10 /* form: STM32F4-Discovery_FW_V1.1.0/Project/Peripheral_Examples/IO_Toggle/TrueSTUDIO/IO_Toggle/stm32_flash.ld */
11 MEMORY
12 {
13   FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 1024K
14   SRAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 192K
15   MEMORY_B1 (rx)  : ORIGIN = 0x60000000, LENGTH = 0K
16 }
17 
18 
19 SECTIONS
20 {
21   .text :
22   {
23     KEEP(*(.isr_vector .isr_vector.*))
24     *(.text .text.*)
25     *(.rodata .rodata*)
26     _etext = .;
27   } > FLASH
28   .data : AT (_etext)
29   {
30     _data = .;
31     *(.data .data.*)
32     _edata = .;
33   } > SRAM
34   .bss(NOLOAD) :
35   {
36     _bss = .;
37     *(.bss .bss.*)
38     *(COMMON)
39     . = ALIGN(4);
40     _ebss = .;
41   } > SRAM
42   .stackarea(NOLOAD) :
43   {
44     . = ALIGN(8);
45     *(.stackarea .stackares.* .stackares)
46     . = ALIGN(8);
47   } > SRAM
48 
49   . = ALIGN(4);
50   _end = .;
51 }

list 2. arm-none-eabi-objdump -h mygpio_led.elf
 1 
 2 mygpio_led.elf:     file format elf32-littlearm
 3 
 4 Sections:
 5 Idx Name          Size      VMA       LMA       File off  Algn
 6   0 .text         00000138  08000000  08000000  00010000  2**2
 7                   CONTENTS, ALLOC, LOAD, READONLY, CODE
 8   1 .data         00000004  20000000  08000138  00020000  2**2
 9                   CONTENTS, ALLOC, LOAD, DATA
10   2 .bss          00000000  20000004  0800013c  00020004  2**0
11                   ALLOC
12   3 .stackarea    00000104  20000004  0800013c  00020004  2**2
13                   ALLOC
14   4 .debug_info   000001d8  00000000  00000000  00020004  2**0
15                   CONTENTS, READONLY, DEBUGGING
16   5 .debug_abbrev 00000137  00000000  00000000  000201dc  2**0
17                   CONTENTS, READONLY, DEBUGGING
18   6 .debug_aranges 00000020  00000000  00000000  00020313  2**0
19                   CONTENTS, READONLY, DEBUGGING
20   7 .debug_line   0000008b  00000000  00000000  00020333  2**0
21                   CONTENTS, READONLY, DEBUGGING
22   8 .debug_str    00000156  00000000  00000000  000203be  2**0
23                   CONTENTS, READONLY, DEBUGGING
24   9 .comment      00000031  00000000  00000000  00020514  2**0
25                   CONTENTS, READONLY
26  10 .ARM.attributes 00000033  00000000  00000000  00020545  2**0
27                   CONTENTS, READONLY
28  11 .debug_frame  000000f8  00000000  00000000  00020578  2**2
29                   CONTENTS, READONLY, DEBUGGING


觀察 list 2. 就可以發現 .data section VMA 是 20000000, LMA 是 08000138。mygpio_led.c L4 變數 val 就是位於這裡, 那應該會有個疑問, 當我 printf &val 時, 位址是 20000000 還是 08000138?

list 5. L28 拿掉 AT (_etext), 再觀察 list 6. L8 就可以發現 .data section VMA, LMA 都是 20000000。

list 5. stm32.ld
 1 /*
 2 MEMORY
 3 {
 4   FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 128K
 5   SRAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K
 6 }
 7 */
 8 
 9 /* Specify the memory areas */
10 /* form: STM32F4-Discovery_FW_V1.1.0/Project/Peripheral_Examples/IO_Toggle/TrueSTUDIO/IO_Toggle/stm32_flash.ld */
11 MEMORY
12 {
13   FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 1024K
14   SRAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 192K
15   MEMORY_B1 (rx)  : ORIGIN = 0x60000000, LENGTH = 0K
16 }
17 
18 
19 SECTIONS
20 {
21   .text :
22   {
23     KEEP(*(.isr_vector .isr_vector.*))
24     *(.text .text.*)
25     *(.rodata .rodata*)
26     _etext = .;
27   } > FLASH
28   .data : 
29   {
30     _data = .;
31     *(.data .data.*)
32     _edata = .;
33   } > SRAM
34   .bss(NOLOAD) :
35   {
36     _bss = .;
37     *(.bss .bss.*)
38     *(COMMON)
39     . = ALIGN(4);
40     _ebss = .;
41   } > SRAM
42   .stackarea(NOLOAD) :
43   {
44     . = ALIGN(8);
45     *(.stackarea .stackares.* .stackares)
46     . = ALIGN(8);
47   } > SRAM
48 
49   . = ALIGN(4);
50   _end = .;
51 }

list 6. arm-none-eabi-objdump -h mygpio_led.elf
 1 
 2 mygpio_led.elf:     file format elf32-littlearm
 3 
 4 Sections:
 5 Idx Name          Size      VMA       LMA       File off  Algn
 6   0 .text         00000138  08000000  08000000  00010000  2**2
 7                   CONTENTS, ALLOC, LOAD, READONLY, CODE
 8   1 .data         00000004  20000000  20000000  00020000  2**2
 9                   CONTENTS, ALLOC, LOAD, DATA
10   2 .bss          00000000  20000004  20000004  00020004  2**0
11                   ALLOC
12   3 .stackarea    00000104  20000004  20000004  00020004  2**2
13                   ALLOC
14   4 .debug_info   000001d8  00000000  00000000  00020004  2**0
15                   CONTENTS, READONLY, DEBUGGING
16   5 .debug_abbrev 00000137  00000000  00000000  000201dc  2**0
17                   CONTENTS, READONLY, DEBUGGING
18   6 .debug_aranges 00000020  00000000  00000000  00020313  2**0
19                   CONTENTS, READONLY, DEBUGGING
20   7 .debug_line   0000008b  00000000  00000000  00020333  2**0
21                   CONTENTS, READONLY, DEBUGGING
22   8 .debug_str    00000156  00000000  00000000  000203be  2**0
23                   CONTENTS, READONLY, DEBUGGING
24   9 .comment      00000031  00000000  00000000  00020514  2**0
25                   CONTENTS, READONLY
26  10 .ARM.attributes 00000033  00000000  00000000  00020545  2**0
27                   CONTENTS, READONLY
28  11 .debug_frame  000000f8  00000000  00000000  00020578  2**2
29                   CONTENTS, READONLY, DEBUGGING


val 變數位址是 0x20000000 還是 0x08000138 有什麼差異呢? 這大概要有碰過 embedded system 的開發人員才有體會, 0x08000138 是 flash 的位址, 讀的時候沒問題, 可以 int c = val, 但是寫的話 val = 6, 是無法作到的 (ddr 位址範圍才可以這麼做), flash 有一套寫的指令, 需要一點複雜的操作才可以寫入, 但對 c 程式員來說, val=6 就要完成這個操作, c 程式員不應該還要去煩心這是 flash 還是 ddr 位址。

所以當作 val = 6 時, 是使用 0x20000000 位址, 也就是 6 會寫到 ddr 0x20000000 的位址上。那 0x08000138 是幹麻用的? 如果你 printf("val: %#x\n", val), 應該會預期要得到 0x123456ab, 但開機時, ddr 應該是隨機值, 有可能會是 0x123456ab 嗎? 所以 c runtime 有一段 code 把 0x123456ab 複製到 0x20000000, 那麼要從哪裡找到 0x123456ab, 答對了, 就是從 0x08000138 把 0x123456ab 複製到 0x20000000 (stm32.h L20, 21)。放在 data section 的變數都要這樣處理, 這就是 c runtime 的祕密, 每個平台的 c runtime 要做的事情都不同, 像是 rpi2 就不用這麼做, 這是 cortex M 獨有的。

這引發另外一個問題, 要怎麼確定把這個執行檔燒錄到 flash 時, 0x08000138 正好是 0x123456ab? list 7 0x138 剛好是 123456ab, 這個就是 val, 在寫入 mygpio_led.bin 到 flash 位址 0x08000000, 就變成 0x08000138, 這就是整個被 linker script 安排好的魔法, 除了靠 linker script, 也要靠 c runtime 的 code。

list 7. hexdump -C mygpio_led.bin
 1 00000000  08 01 00 20 41 00 00 08  f5 00 00 08 f5 00 00 08  |... A...........|
 2 00000010  f5 00 00 08 f5 00 00 08  f5 00 00 08 f5 00 00 08  |................|
 3 00000020  f5 00 00 08 f5 00 00 08  f5 00 00 08 c5 00 00 08  |................|
 4 00000030  f5 00 00 08 f5 00 00 08  a1 00 00 08 e9 00 00 08  |................|
 5 00000040  80 b5 82 b0 00 af 11 4b  7b 60 11 4b 3b 60 07 e0  |.......K{`.K;`..|
 6 00000050  3b 68 1a 1d 3a 60 7a 68  11 1d 79 60 12 68 1a 60  |;h..:`zh..y`.h.`|
 7 00000060  3b 68 0c 4a 93 42 f3 d3  0b 4b 3b 60 04 e0 3b 68  |;h.J.B...K;`..;h|
 8 00000070  1a 1d 3a 60 00 22 1a 60  3b 68 08 4a 93 42 f6 d3  |..:`.".`;h.J.B..|
 9 00000080  00 f0 50 f8 00 bf 08 37  bd 46 80 bd 38 01 00 08  |..P....7.F..8...|
10 00000090  00 00 00 20 04 00 00 20  04 00 00 20 04 00 00 20  |... ... ... ... |
11 000000a0  80 b5 82 b0 00 af 05 23  7b 60 7b 68 01 33 7b 60  |.......#{`{h.3{`|
12 000000b0  03 48 00 f0 25 f8 00 bf  08 37 bd 46 80 bd 00 bf  |.H..%....7.F....|
13 000000c0  ff ff 3f 00 80 b5 00 af  05 4a 05 4b 1b 68 43 f0  |..?......J.K.hC.|
14 000000d0  80 53 13 60 03 48 00 f0  13 f8 00 bf 80 bd 00 bf  |.S.`.H..........|
15 000000e0  04 ed 00 e0 ff ff 3f 00  80 b4 00 af 00 bf bd 46  |......?........F|
16 000000f0  80 bc 70 47 80 b4 00 af  00 bf bd 46 80 bc 70 47  |..pG.......F..pG|
17 00000100  80 b4 85 b0 00 af 78 60  05 23 fb 60 02 e0 7b 68  |......x`.#.`..{h|
18 00000110  01 3b 7b 60 fb 68 00 2b  f9 d1 00 bf 14 37 bd 46  |.;{`.h.+.....7.F|
19 00000120  80 bc 70 47 80 b5 00 af  02 48 ff f7 e9 ff 00 23  |..pG.....H.....#|
20 00000130  18 46 80 bd ff ff 3f 00  ab 56 34 12              |.F....?..V4.|
21 0000013c


另外 bss section 的值都要是 0, 也是用類似的作法完成的, 取得 bss 開始位址和結束位址 (list 1 L34 ~ L41, stm32.h L22 ~ L23), 把整個 bss 設定為 0 (stm32.h L22, 23)。cortex M 只要做這樣, 就完成 c runtime, 之後就可以用 c 語言了。當然, 如果寫程式都只用組合語言, 就不必做這些事情。

stm32.h
  6 #define STACK_SIZE 64
  7 extern unsigned long _etext;
  8 extern unsigned long _data;
  9 extern unsigned long _edata;
 10 extern unsigned long _bss;
 11 extern unsigned long _ebss;
 12 
 13 int main(void);
 14 
 15 void ResetISR(void)
 16 {
 17   unsigned long *pulSrc, *pulDest;
 18 
 19   pulSrc = &_etext;
 20   for (pulDest = &_data; pulDest < &_edata;)
 21     *pulDest++ = *pulSrc++;
 22   for (pulDest = &_bss; pulDest < &_ebss;)
 23     *pulDest++ = 0;
 24 
 25   main();
 26 }


linker script 當然不是只有這麼簡單, 這只是很基本的介紹。

ref:
GNU LD 手冊略讀 (2): Chapter 3.6 SETCIONS (這是一位朋友寫的, 前陣子得知其往生的消息, 令人感傷, 感謝他的文章)

沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。