本文主要介绍在ARM汇编中使用 Libc 和半主机调试
关于 ARM 汇编快速入门,请 参考
start.s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| .syntax unified
/* 代码段 */ .section .text.reset .type reset, %function .globl reset
/* 程序入口 */ reset:
ldr sp, =_estack
/* 将 data 段从 flash 拷贝到 SRAM */ ldr r0, =_data_start ldr r1, =_data_end ldr r2, =_init_data_start movs r3, #0 b copy_data_loop
copy_data: ldr r4, [r2, r3] // r4 = *(_init_data_start + r3) str r4, [r0, r3] // *(_data_start + r3) = r4 adds r3, r3, #4 // r3 += 4
copy_data_loop: adds r4, r0, r3 // r4 = _data_start + r3 cmp r4, r1 bcc copy_data // if(r4 != r1) copy_data
/* 将 0 填充到 .bss 段中 */ ldr r2, =_bss_start ldr r4, =_bss_end movs r3, #0 b fill_zero_loop
fill_zero: str r3, [r2] // *(r2) = 0 adds r2, r2, #4 // r2 += 4
fill_zero_loop: cmp r2, r4 bcc fill_zero // if(r2 != _bss_end) fill_zero
/* 调用 libc 的静态初始化 */ bl __libc_init_array
/* 调用应用入口点 */ bl main bx lr
/* 中断向量表段 */ .section .vectors, "a" .word _estack .word reset
|
main.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| #include <stdint.h> #include <stdio.h> #include <stdlib.h> #include <string.h>
int aaa = 12345;
void *_sbrk(ptrdiff_t incr) { extern char _end[]; extern char _heap_end[]; static char *curbrk = _end;
if ((curbrk + incr < _end) || (curbrk + incr > _heap_end)) return NULL - 1;
curbrk += incr; return curbrk - incr; }
extern void initialise_monitor_handles(void);
int main(int argc, char const *argv[]) { initialise_monitor_handles();
char *data = malloc(0x2000 - 0x100); if (data == NULL) return -1;
strcpy(data, "I am Jack"); printf("Hello World, %s, aaa is %d\n", data, aaa);
extern uint32_t _data_start; extern uint32_t _data_end; extern uint32_t _bss_start; extern uint32_t _bss_end; extern uint32_t _heap_start; extern uint32_t _init_data_start;
printf("_data_start %p\n", &_data_start); printf("_data_end %p\n", &_data_end); printf("_bss_start %p\n", &_bss_start); printf("_bss_end %p\n", &_bss_end); printf("_heap_start %p\n", &_heap_start); printf("_init_data_start %p\n", &_init_data_start);
char buf[64]; scanf("%s", buf);
printf("%s\n", buf);
return 0; }
|
link.ld
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| /* 程序入口 */ ENTRY(reset)
/* 栈指针初始位置 */ _estack = ORIGIN(DTCMRAM) + LENGTH(DTCMRAM);
/* 用于预估堆栈大小 */ _heap_size = 0x2000; _stack_size = 0x4000;
/* 存储器分布 */ MEMORY { DTCMRAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 128K }
/* 段组织 */ SECTIONS { /* 代码 */ .text : { KEEP(*(.vectors)) *(.text) *(.text.*)
KEEP (*(.init)) KEEP (*(.fini))
} >FLASH
/* 只读数据 */ .rodata : { . = ALIGN(4); *(.rodata) *(.rodata*) } >FLASH
/* 将读写数据段放到 data 段 */ .data : { . = ALIGN(4); _data_start = .;
*(.data) *(.data*)
. = ALIGN(4); _data_end = .; } >DTCMRAM AT> FLASH
/* 指向 flash 中用于初始化的 data */ _init_data_start = LOADADDR(.data);
/* 未初始化数据放到 bss 段 */ .bss : { . = ALIGN(4); _bss_start = .; __bss_start__ = _bss_start; *(.bss) *(.bss*) *(COMMON)
. = ALIGN(4); _bss_end = .; __bss_end__ = _bss_end; } >DTCMRAM
/* 堆 */ .heap : { . = ALIGN(8); _heap_start = .; PROVIDE ( end = . ); PROVIDE ( _end = . ); . = . + _heap_size; _heap_end = .; . = . + _stack_size; . = ALIGN(8); } >DTCMRAM }
|
Makefile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| CC = arm-none-eabi-gcc
LDFLAGS = -mcpu=cortex-m7 -lc -lrdimon -Tlink.ld -Wl,--gc-sections
CFLAGS = -mcpu=cortex-m7 -g
default: @mkdir build -p $(CC) -c $(CFLAGS) main.c -o build/main.o $(CC) -c $(CFLAGS) start.s -o build/start.o $(CC) build/start.o build/main.o $(LDFLAGS) -Wl,-Map=build/output.map,--cref -o build/demo.elf
openocd: make default && openocd
clean: rm build -rf
|
openocd.cfg
1 2 3 4 5 6
| source [find interface/stlink.cfg] source [find target/stm32h7x.cfg]
reset_config none separate
program build/demo.elf verify
|
1. 使用 C
1.1 初始化栈
进入 C 的世界栈是必须的,在定义中断向量表时设置了第一个 word
的值就是栈的初始地址 _estack,
1 2 3
| .section .isr_vector .word _estack .word reset
|
_estack 在 ld 文件中被指向为 DTCMRAM 的末尾,因为 ARM
体系中,栈是向低地址伸长,因此需要放在内存末尾处:
1
| _estack = ORIGIN(DTCMRAM) + LENGTH(DTCMRAM);
|
中断向量表会被连接到 FLASH 的最开始地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| /* 段组织 */ SECTIONS { /* 代码 */ .text : { KEEP(*(.vectors)) *(.text) *(.text.*)
KEEP (*(.init)) KEEP (*(.fini))
} >FLASH
... }
|
STM32 复位后,BOOT=0 时,CPU 会从 FLASH 地址的第一个 word 处加载 sp
指针,从第二个 word 处加载 pc
指针,从而转跳到用户代码,因此这里只需要在中断向量表的第一个 word
填入栈地址,C 的环境便建立好了。
但是一般来说,为了保险,在进入用户代码的第一时间,我们还是需要主动设置一下
sp 地址,因为有可能 reset 函数不是通过硬件复位进去的,这时 sp
可能已经更改,而且由于是软件调用复位, CPU 不会自动加载向量表中的 sp
值,因此需要第一时间重写设置 sp 寄存器:
1 2 3 4 5 6 7 8
| .section .text .type reset, %function .globl reset
/* 程序入口 */ reset: ldr sp, =_estack
|
ldr 属于 THUMB 指令格式,需要在顶部指示 .syntax
unified,说明下面的指令是 ARM 和 THUMB 通用格式的
1.2 初始化内存变量
虽然这时候可以进入 C 的环境了,但是 C
中用到的内存和变量都还是随机,并没有进行初始化和赋值。这样一来,C
中定义的一些全局变量可能无法正常使用,因此这一步需要在汇编中将变量进行初始化和赋值。
一般来说,为了方便一次性对各个 C
文件中的静态或全局变量进行初始化,在链接器中需要对它们进行合理的安排:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| /* 将读写数据段放到 data 段 */ .data : { . = ALIGN(4); _data_start = .;
*(.data) *(.data*)
. = ALIGN(4); _data_end = .; } >DTCMRAM AT> FLASH
/* 指向 flash 中用于初始化的 data */ _init_data_start = LOADADDR(.data);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| /* 未初始化数据放到 bss 段 */ .bss : { . = ALIGN(4); _bss_start = .; __bss_start__ = _bss_start; *(.bss) *(.bss*) *(COMMON)
. = ALIGN(4); _bss_end = .; __bss_end__ = _bss_end; } >DTCMRAM
|
在 ld 中确定好位置后在 start.s 中进行初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| /* 将 data 段从 flash 拷贝到 SRAM */ ldr r0, =_data_start ldr r1, =_data_end ldr r2, =_init_data_start movs r3, #0 b copy_data_loop
copy_data: ldr r4, [r2, r3] // r4 = *(_init_data_start + r3) str r4, [r0, r3] // *(_data_start + r3) = r4 adds r3, r3, #4 // r3 += 4
copy_data_loop: adds r4, r0, r3 // r4 = _data_start + r3 cmp r4, r1 bcc copy_data // if(r4 != r1) copy_data
/* 将 0 填充到 .bss 段中 */ ldr r2, =_bss_start ldr r4, =_bss_end movs r3, #0 b fill_zero_loop
fill_zero: str r3, [r2] // *(r2) = 0 adds r2, r2, #4 // r2 += 4
fill_zero_loop: cmp r2, r4 bcc fill_zero // if(r2 != _bss_end) fill_zero
|
2. 使用 libc
参考
arm-none-eabi-gcc 默认使用的是 newlib, 前面已经初始化好 data 和 bss
段,这里只需在 start 中调用 libc 的初始化函数即可
1 2
| /* 调用 libc 的静态初始化 */ bl __libc_init_array
|
__libc_init_array 函数开始时会调用 .init 中的段,结束时调用
.fini 中的段,因此在 .text 中添加
1 2
| KEEP (*(.init)) KEEP (*(.fini))
|
3. 使用堆
要使用标准库的 malloc, 需要指定堆内存地址,默认情况下标准库使用 _end
作为堆的起始地址,_end 表示 data 和 bss 结束后的内存地址,即除了 data
bss, 剩余的都分给了堆栈
1 2 3 4 5 6 7 8 9 10 11 12
| /* 堆 */ .heap : { . = ALIGN(8); _heap_start = .; PROVIDE ( end = . ); PROVIDE ( _end = . ); . = . + _heap_size; _heap_end = .; . = . + _stack_size; . = ALIGN(8); } >DTCMRAM
|
我们把栈放在堆后面,为防止堆栈重叠,或超过可用内存,可预先设置堆栈大小,随着应用变量的使用,堆的初始地址边界会随之向高地址移动,当超过了
RAM 大小,链接时就会报错。
malloc 会调用 _sbrk
函数来确认内存大小,根据链接脚本中的堆地址和大小我们可以实现:
1 2 3 4 5 6 7 8 9 10 11 12
| void *_sbrk(ptrdiff_t incr) { extern char _end[]; extern char _heap_end[]; static char *curbrk = _end;
if ((curbrk + incr < _end) || (curbrk + incr > _heap_end)) return NULL - 1;
curbrk += incr; return curbrk - incr; }
|
4. 使用半主机模式进行调试
要使用半主机,可链接到 -lrdimon
,librdimon
提供了一系列系统调用函数和 GDB
进行通信,从而实现在调试时执行标准输入输出和文件读写。
半主机模式使用前需进行初始化,使用 initialise_monitor_handles
进行初始化,然后就可以愉快地使用 printf 和 fopen 等函数了。
1 2 3 4 5 6 7 8
| extern void initialise_monitor_handles(void);
int main(int argc, char const *argv[]) { initialise_monitor_handles();
... }
|
当然不要忘记在 GDB 中使能半主机调试,若是用 VSCode 可直接在
setupCommands 中添加
1
| {"text": "monitor arm semihosting enable"}
|
5. Map 文件
参考
map 文件包含了链接的各个 obj
文件的输出情况以及各个段地址的详情信息等,在链接时通过参数
-Wl,-Map=build/output.map,--cref
可输出 map 文件。 参考
2
--cref
可生成交叉引用列表(每个符号出自哪个目标文件,如果还有指定-Map,则会添加到
map 文件中。否则,打印到标准输出)
6. Glibc 中的特殊的段
.init |
一个函数放到。init 段,在 main 函数执行前系统就会执行它 |
.fini |
假如一个函数放到。fini 段,在 main 函数返回后该函数就会被执行 |
__libc_init_array
这个函数中执行的关键过程如下:
调用 .preinit_array 段中的预初始化函数 调用 .init 段中的 _init 函数
调用 .init_array 中的所有函数
这里我需要解释下,在一个段中可能会存在多个函数,根据链接脚本的写法,链接器会在链接时将段名相同的函数指针放到同一个段中,然后通过在段的前后设定锚点来依次执行相应的函数,这样的方式在一些系统的驱动初始化中也能见到!
与 __libc_init_array 对应的函数是 __libc_fini_array,这个函数是在
main 函数执行完成后执行的。它首先会调用 .fini_array
中的所有函数,然后调用 _fini 函数,在嵌入式中一般不会执行到
__libc_fini_array 函数。
在 pc 端也有类似的过程,_init 与 _fini 函数在 crti.o 中被定义。
源文件来自于