WWTD's Blog

Welcome to my blog

前言

对于MCU也就是微控制器而言,Arm cortex M系列的MCU几乎是市场里的标杆和模范生了。为了更深入的理解cortex M3/M4及这一类型的MCU,因此选择了《ARM Cortex-M3 与 Cortex-M4 权威指南》第三版,来做深入的学习。

这本书我在不同的渠道收到多次推荐,应该是业界非常有名的指导书了,希望能有满意的收获。

在学习的过程中,可能会借由实验来验证或者加深理解,实验会尽可能通过虚拟环境来做。

关于cortex M3、M4

Cortex M3、M4均由ARM公司设计,两者都是32位的微处理器,其中M3发布于2005、2006年,M4发布于2010年。两者的主要差异在于,M4处理器支持浮点运算并拥有更好的DSP性能。
ARM 文档中心

实验环境搭建

得益于前人的智慧,对于cortex M的交叉编译、模拟、调试都有比较成熟的方案了,我的实验平台是一个x86的debian小主机,会主要通过arm-none-eabi-gcc系列交叉编译;通过qemu做模拟;通过gdb-multiarch做调试。
使用工具的具体版本如下:

工具 版本 用途
qemu-system-arm 7.2.22 Cortex-M3/M4 机器模拟
arm-none-eabi-gcc 12.2.1 ARM 裸机交叉编译器
gdb-multiarch 13.1 多架构 GDB 调试器
libnewlib-arm-none-eabi 3.3.0 裸机 C 库

而对于实验code的基础布局如下:

1
2
3
4
5
6
projects/01-env/
├── Makefile # 构建系统
├── linker.ld # 链接脚本
├── startup.c # 向量表 + 启动代码
├── main.c # 测试程序
└── debug.gdb # GDB 调试脚本

各自细节如:

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
#Makefile
CFLAGS = -Wall -Wextra -g -O0 -ffreestanding -nostdlib -mthumb
LDFLAGS = -Wl,-T,linker.ld -nostartfiles -nostdlib -lgcc

SRCS = startup.c main.c

.PHONY: all clean m3 m4 qemu-m3 qemu-m4 gdb-m3 gdb-m4

all: m3 m4

m3: test-m3.elf
m4: test-m4.elf

test-m3.elf: $(SRCS) linker.ld
arm-none-eabi-gcc $(CFLAGS) -mcpu=cortex-m3 $(LDFLAGS) -o $@ $(SRCS)

test-m4.elf: $(SRCS) linker.ld
arm-none-eabi-gcc $(CFLAGS) -mcpu=cortex-m4 -mfloat-abi=soft $(LDFLAGS) -o $@ $(SRCS)

qemu-m3: test-m3.elf
@echo "=== Cortex-M3 (lm3s6965evb) ==="
qemu-system-arm -M lm3s6965evb -kernel test-m3.elf -nographic

qemu-m4: test-m4.elf
@echo "=== Cortex-M4 (mps2-an386) ==="
qemu-system-arm -M mps2-an386 -kernel test-m4.elf -nographic

qemu-gdb-m3: test-m3.elf
@echo "=== QEMU waiting for GDB on :1234 (M3) ==="
qemu-system-arm -M lm3s6965evb -kernel test-m3.elf -S -gdb tcp::1234 -nographic

qemu-gdb-m4: test-m4.elf
@echo "=== QEMU waiting for GDB on :1234 (M4) ==="
qemu-system-arm -M mps2-an386 -kernel test-m4.elf -S -gdb tcp::1234 -nographic

gdb:
gdb-multiarch -q -ex "target remote :1234" test-m3.elf

clean:
rm -f *.o *.elf *.map
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
# linker.ld
ENTRY(Reset_Handler)

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

SECTIONS
{
.vectors : {
KEEP(*(.vectors))
} > FLASH

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

.data : {
_sdata = .;
*(.data*)
. = ALIGN(4);
_edata = .;
} > SRAM AT > FLASH
_sidata = LOADADDR(.data);

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

. = ALIGN(4);
_end = .;
}
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
// startup.c
extern unsigned int _sdata, _edata, _sbss, _ebss;
extern unsigned int _sidata;
extern int main(void);

__attribute__((naked)) void Reset_Handler(void) {
unsigned int *src, *dst;

src = &_sidata;
dst = &_sdata;
while (dst < &_edata)
*dst++ = *src++;

dst = &_sbss;
while (dst < &_ebss)
*dst++ = 0;

main();

while (1);
}

void Default_Handler(void) {
while (1);
}

void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void MemManage_Handler(void) __attribute__((weak, alias("Default_Handler")));
void BusFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void UsageFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SVC_Handler(void) __attribute__((weak, alias("Default_Handler")));
void DebugMon_Handler(void) __attribute__((weak, alias("Default_Handler")));
void PendSV_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_Handler")));

__attribute__((used, section(".vectors")))
void *vector_table[16] = {
[0] = (void *)0x20010000,
[1] = (void *)Reset_Handler,
[2] = (void *)NMI_Handler,
[3] = (void *)HardFault_Handler,
[4] = (void *)MemManage_Handler,
[5] = (void *)BusFault_Handler,
[6] = (void *)UsageFault_Handler,
[11] = (void *)SVC_Handler,
[12] = (void *)DebugMon_Handler,
[14] = (void *)PendSV_Handler,
[15] = (void *)SysTick_Handler,
};
1
2
3
4
5
6
7
8
9
10
//main.c
volatile unsigned int counter;

int main(void) {
counter = 0;
while (1) {
counter++;
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# debug.gdb
# 启动方式:
# terminal 1: make qemu-gdb-m3 (或 qemu-gdb-m4)
# terminal 2: gdb-multiarch -x debug.gdb
#
# 然后即可调试:
# (gdb) break main # 设置断点
# (gdb) continue # 运行到断点
# (gdb) stepi # 单步执行
# (gdb) info registers # 查看寄存器
# (gdb) print counter # 查看变量
# (gdb) x/4xw 0x20000000 # 查看内存

file test-m3.elf
target remote :1234

Makefile 目标

目标 说明
make m3 编译 Cortex-M3 ELF
make m4 编译 Cortex-M4 ELF
make qemu-m3 直接运行 M3 (无调试)
make qemu-m4 直接运行 M4 (无调试)
make qemu-gdb-m3 M3 + GDB 等待连接 (:1234)
make qemu-gdb-m4 M4 + GDB 等待连接 (:1234)
make gdb 连接 GDB 到 QEMU

QEMU 机器选型

  • Cortex-M3: lm3s6965evb (TI Stellaris LM3S6965)
  • Cortex-M4: mps2-an386 (ARM MPS2 + AN386 FPGA)

两者均将代码映射到 0x00000000,SRAM 映射到 0x20000000,便于用同一套链接脚本。

  • 如果希望使用其他机器,可以使用如下指令列出支持列表
    1
    qemu-system-arm -M help

实验环境验证

1
2
3
4
# terminal 1: 启动 QEMU (等待 GDB)
make qemu-gdb-m3
# terminal 2: 连接 GDB
make gdb

实验1:从 Reset 到 main——Cortex-M3 启动全流程跟踪

目标:结合反汇编与 GDB,完整跟踪 Cortex-M3 从上电复位到用户 main 函数执行的每一步,观察向量表布局、启动代码对 .bss 的初始化、函数调用时 LR 的变化,以及寄存器在循环中的活动。
前提:已完成环境搭建,能正常编译和启动 QEMU+GDB。

中断向量表与Reset_Handler

1
2
3
4
5
6
7
8
9
10
# 编译M3的elf
make m3
# 查看向量表
arm-none-eabi-objdump -s -j .vectors test-m3.elf
test-m3.elf: file format elf32-littlearm
Contents of section .vectors:
0000 00000120 41000000 89000000 89000000 ... A...........
0010 89000000 89000000 89000000 00000000 ................
0020 00000000 00000000 00000000 89000000 ................
0030 89000000 00000000 89000000 89000000 ................

因为cortex-M3 是小端序的,所以实际这个表阅读方式是

偏移 值 (LE) 对应异常 说明
0x0000 0x20010000 初始 MSP 栈顶地址
0x0004 0x00000041 Reset_Handler 地址 0x40,bit 0 = 1 (Thumb)
0x0008 0x00000089 NMI_Handler 地址 0x88 (= Default_Handler)
0x000C 0x00000089 HardFault_Handler 同上,弱符号 alias
0x002C 0x00000089 SVC_Handler
0x0030 0x00000089 DebugMon_Handler
0x0038 0x00000089 PendSV_Handler
0x003C 0x00000089 SysTick_Handler

这里就是和我们startup.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
__attribute__((naked)) void Reset_Handler(void) {
/*
something
*/
}

void Default_Handler(void) {
while (1);
}

void NMI_Handler(void) __attribute__((weak, alias("Default_Handler")));
void HardFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void MemManage_Handler(void) __attribute__((weak, alias("Default_Handler")));
void BusFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void UsageFault_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SVC_Handler(void) __attribute__((weak, alias("Default_Handler")));
void DebugMon_Handler(void) __attribute__((weak, alias("Default_Handler")));
void PendSV_Handler(void) __attribute__((weak, alias("Default_Handler")));
void SysTick_Handler(void) __attribute__((weak, alias("Default_Handler")));

__attribute__((used, section(".vectors")))
void *vector_table[16] = {
[0] = (void *)0x20010000,
[1] = (void *)Reset_Handler,
[2] = (void *)NMI_Handler,
[3] = (void *)HardFault_Handler,
[4] = (void *)MemManage_Handler,
[5] = (void *)BusFault_Handler,
[6] = (void *)UsageFault_Handler,
[11] = (void *)SVC_Handler,
[12] = (void *)DebugMon_Handler,
[14] = (void *)PendSV_Handler,
[15] = (void *)SysTick_Handler,
};

稍微有一点值得提出的是,这里的函数地址bit0都被置为1了,这是因为cortex-M要求Thumb模式,而在取出PC后,硬件是会自动清除bit0的。
关于Reset_Handler,startup.c里我们写的其实是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
extern unsigned int _sdata, _edata, _sbss, _ebss;
extern unsigned int _sidata;
extern int main(void);
__attribute__((naked)) void Reset_Handler(void) {
unsigned int *src, *dst;

src = &_sidata;
dst = &_sdata;
while (dst < &_edata)
*dst++ = *src++;

dst = &_sbss;
while (dst < &_ebss)
*dst++ = 0;

main();

while (1);
}

而我们通过反汇编,看一下实际编译生成的汇编代码其实是:

1
arm-none-eabi-objdump -d test-m3.elf | grep -A 40 '<Reset_Handler>'
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
00000040 <Reset_Handler>:
40: 4d0c ldr r5, [pc, #48] @ (74 <Reset_Handler+0x34>)
42: 4c0d ldr r4, [pc, #52] @ (78 <Reset_Handler+0x38>)
44: e005 b.n 52 <Reset_Handler+0x12>
46: 462a mov r2, r5
48: 1d15 adds r5, r2, #4
4a: 4623 mov r3, r4
4c: 1d1c adds r4, r3, #4
4e: 6812 ldr r2, [r2, #0]
50: 601a str r2, [r3, #0]
52: 4b0a ldr r3, [pc, #40] @ (7c <Reset_Handler+0x3c>)
54: 429c cmp r4, r3
56: d3f6 bcc.n 46 <Reset_Handler+0x6>
58: 4c09 ldr r4, [pc, #36] @ (80 <Reset_Handler+0x40>)
5a: e003 b.n 64 <Reset_Handler+0x24>
5c: 4623 mov r3, r4
5e: 1d1c adds r4, r3, #4
60: 2200 movs r2, #0
62: 601a str r2, [r3, #0]
64: 4b07 ldr r3, [pc, #28] @ (84 <Reset_Handler+0x44>)
66: 429c cmp r4, r3
68: d3f8 bcc.n 5c <Reset_Handler+0x1c>
6a: f000 f811 bl 90 <main>
6e: bf00 nop
70: e7fd b.n 6e <Reset_Handler+0x2e>
72: bf00 nop
74: 000000ac .word 0x000000ac
78: 20000000 .word 0x20000000
7c: 20000000 .word 0x20000000
80: 20000000 .word 0x20000000
84: 20000004 .word 0x20000004

00000088 <Default_Handler>:
88: b480 push {r7}
8a: af00 add r7, sp, #0
8c: bf00 nop
8e: e7fd b.n 8c <Default_Handler+0x4>

00000090 <main>:
90: b480 push {r7}
92: af00 add r7, sp, #0

代码并不算长,我们尝试解读一下

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
00000040 <Reset_Handler>:
40: 4d0c ldr r5, [pc, #48] @ r5 = _sidata (0xac)
42: 4c0d ldr r4, [pc, #52] @ r4 = _sdata (0x20000000)
44: e005 b.n 52 @ 跳转到条件检查
; .data 复制循环入口:
46: 462a mov r2, r5
48: 1d15 adds r5, r2, #4 @ src++
4a: 4623 mov r3, r4
4c: 1d1c adds r4, r3, #4 @ dst++
4e: 6812 ldr r2, [r2, #0] @ r2 = *src
50: 601a str r2, [r3, #0] @ *dst = r2

52: 4b0a ldr r3, [pc, #40] @ r3 = _edata (0x20000000)
54: 429c cmp r4, r3
56: d3f6 bcc.n 46 @ if dst < _edata, 继续复制
; .bss 清零循环入口:
58: 4c09 ldr r4, [pc, #36] @ r4 = _sbss (0x20000000)
5a: e003 b.n 64 @ 跳转到条件检查

5c: 4623 mov r3, r4 @ r3 = dst
5e: 1d1c adds r4, r3, #4 @ dst += 4
60: 2200 movs r2, #0 @ r2 = 0
62: 601a str r2, [r3, #0] @ *dst = 0

64: 4b07 ldr r3, [pc, #28] @ r3 = _ebss (0x20000004)
66: 429c cmp r4, r3
68: d3f8 bcc.n 5c @ if dst < _ebss, 继续清零

6a: f000 f811 bl 90 @ 调用 main
6e: bf00 nop
70: e7fd b.n 6e @ main 返回后死循环
72: bf00 nop
74: 000000ac .word 0x000000ac
78: 20000000 .word 0x20000000
7c: 20000000 .word 0x20000000
80: 20000000 .word 0x20000000
84: 20000004 .word 0x20000004

00000088 <Default_Handler>:
88: b480 push {r7}
8a: af00 add r7, sp, #0
8c: bf00 nop
8e: e7fd b.n 8c <Default_Handler+0x4>

00000090 <main>:
90: b480 push {r7}
92: af00 add r7, sp, #0

这里因为用到了链接脚本中的几个变量,这些变量的值也可以通过符号表二次确认:

1
2
3
4
5
6
7
arm-none-eabi-objdump -t test-m3.elf | grep -E "counter|_s[bd]|_e[bd]|_sidata"
000000ac g *ABS* 00000000 _sidata
20000000 g .bss 00000000 _sbss
20000000 g .data 00000000 _sdata
20000004 g .bss 00000000 _ebss
20000000 g O .bss 00000004 counter
20000000 g .data 00000000 _edata

能看出来,bss段唯一的一个4字节变量就是counter,所以Reset_Handler 也就是把它清零了。

通过GDB观察启动过程

两个终端,分别执行:

1
make qemu-gdb-m3
1
gdb-multiarch -q

然后在弹出来的gdb交互式窗口分别执行:

1
2
3
file test-m3.elf
target remote :1234
info registers
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
gdb-multiarch -q
(gdb) file test-m3.elf
Reading symbols from test-m3.elf...
(gdb) target remote :1234
Remote debugging using :1234
Reset_Handler () at startup.c:8
8 src = &_sidata;
(gdb) info registers
r0 0x0 0
r1 0x0 0
r2 0x0 0
r3 0x0 0
r4 0x0 0
r5 0x0 0
r6 0x0 0
r7 0x0 0
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0x0 0
sp 0x20010000 0x20010000
lr 0xffffffff -1
pc 0x40 0x40 <Reset_Handler>
xpsr 0x41000000 1090519040

这里其实就能看到,PC在0x40表示Reset_Handler。SP在0x20010000,是我们设置的初始栈指针。LR指向0xffffffff,表示在Thread模式使用MSP。XPSR在0x41000000,其bit24为1表示运行在Thumb模式。
接下来,可以用内存dump工具来看向量表在内存的布局。

1
2
3
4
5
(gdb) x/16xw 0x00000000
0x0 <vector_table>: 0x20010000 0x00000041 0x00000089 0x00000089
0x10 <vector_table+16>: 0x00000089 0x00000089 0x00000089 0x00000000
0x20 <vector_table+32>: 0x00000000 0x00000000 0x00000000 0x00000089
0x30 <vector_table+48>: 0x00000089 0x00000000 0x00000089 0x00000089

和objdump的是完全一致的。

  • 建立自动显示,让每次stepi后自动刷新关键值
    1
    2
    3
    4
    display/i $pc
    display/4xw 0x20000000
    display $lr
    display $r4
    接下来就可以stepi,一路单步执行下去
    1
    2
    3
    4
    stepi
    stepi
    stepi
    stepi
    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
    (gdb) display/i $pc
    1: x/i $pc
    => 0x40 <Reset_Handler>: ldr r5, [pc, #48] @ (0x74 <Reset_Handler+52>)
    (gdb) display/4xw 0x20000000
    2: x/4xw 0x20000000
    0x20000000 <counter>: 0x00000000 0x00000000 0x00000000 0x00000000
    (gdb) display $lr
    3: $lr = -1
    (gdb) display $r4
    4: $r4 = 0
    (gdb) stepi
    9 dst = &_sdata;
    1: x/i $pc
    => 0x42 <Reset_Handler+2>: ldr r4, [pc, #52] @ (0x78 <Reset_Handler+56>)
    2: x/4xw 0x20000000
    0x20000000 <counter>: 0x00000000 0x00000000 0x00000000 0x00000000
    3: $lr = -1
    4: $r4 = 0
    (gdb) stepi
    10 while (dst < &_edata)
    1: x/i $pc
    => 0x44 <Reset_Handler+4>: b.n 0x52 <Reset_Handler+18>
    2: x/4xw 0x20000000
    0x20000000 <counter>: 0x00000000 0x00000000 0x00000000 0x00000000
    3: $lr = -1
    4: $r4 = 536870912
    (gdb) stepi
    10 while (dst < &_edata)
    1: x/i $pc
    => 0x52 <Reset_Handler+18>: ldr r3, [pc, #40] @ (0x7c <Reset_Handler+60>)
    2: x/4xw 0x20000000
    0x20000000 <counter>: 0x00000000 0x00000000 0x00000000 0x00000000
    3: $lr = -1
    4: $r4 = 536870912
    (gdb) stepi
    0x00000054 10 while (dst < &_edata)
    1: x/i $pc
    => 0x54 <Reset_Handler+20>: cmp r4, r3
    2: x/4xw 0x20000000
    0x20000000 <counter>: 0x00000000 0x00000000 0x00000000 0x00000000
    3: $lr = -1
    4: $r4 = 536870912
    reset 芯片,然后给程序的关键地址打上断点,重新观察流程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    (gdb) monitor system_reset
    (gdb) info registers
    r0 0x0 0
    r1 0x0 0
    r2 0x0 0
    r3 0x0 0
    r4 0x0 0
    r5 0xac 172
    r6 0x0 0
    r7 0x0 0
    r8 0x0 0
    r9 0x0 0
    r10 0x0 0
    r11 0x0 0
    r12 0x0 0
    sp 0x20010000 0x20010000
    lr 0xffffffff -1
    pc 0x42 0x42 <Reset_Handler+2>
    xpsr 0x41000000 1090519040
    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
    (gdb) break *0x58
    Breakpoint 1 at 0x58: file startup.c, line 13.
    (gdb) break *0x5c
    Breakpoint 2 at 0x5c: file startup.c, line 15.
    (gdb) break *0x62
    Breakpoint 3 at 0x62: file startup.c, line 15.
    (gdb) break *0x6a
    Breakpoint 4 at 0x6a: file startup.c, line 17.
    (gdb) break main
    Breakpoint 5 at 0x94: file main.c, line 4.
    (gdb) continue
    Continuing.

    Breakpoint 1, Reset_Handler () at startup.c:13
    13 dst = &_sbss;
    1: x/i $pc
    => 0x58 <Reset_Handler+24>: ldr r4, [pc, #36] @ (0x80 <Reset_Handler+64>)
    2: x/4xw 0x20000000
    0x20000000 <counter>: 0x00000000 0x00000000 0x00000000 0x00000000
    3: $lr = -1
    4: $r4 = 536870912
    (gdb) continue
    Continuing.

    Breakpoint 2, Reset_Handler () at startup.c:15
    15 *dst++ = 0;
    1: x/i $pc
    => 0x5c <Reset_Handler+28>: mov r3, r4
    2: x/4xw 0x20000000
    0x20000000 <counter>: 0x00000000 0x00000000 0x00000000 0x00000000
    3: $lr = -1
    4: $r4 = 536870912
    (gdb) continue
    Continuing.

    Breakpoint 3, 0x00000062 in Reset_Handler () at startup.c:15
    15 *dst++ = 0;
    1: x/i $pc
    => 0x62 <Reset_Handler+34>: str r2, [r3, #0]
    2: x/4xw 0x20000000
    0x20000000 <counter>: 0x00000000 0x00000000 0x00000000 0x00000000
    3: $lr = -1
    4: $r4 = 536870916
    (gdb) continue
    Continuing.

    Breakpoint 4, Reset_Handler () at startup.c:17
    17 main();
    1: x/i $pc
    => 0x6a <Reset_Handler+42>: bl 0x90 <main>
    2: x/4xw 0x20000000
    0x20000000 <counter>: 0x00000000 0x00000000 0x00000000 0x00000000
    3: $lr = -1
    4: $r4 = 536870916
    (gdb) continue
    Continuing.

    Breakpoint 5, main () at main.c:4
    4 counter = 0;
    1: x/i $pc
    => 0x94 <main+4>: ldr r3, [pc, #16] @ (0xa8 <main+24>)
    2: x/4xw 0x20000000
    0x20000000 <counter>: 0x00000000 0x00000000 0x00000000 0x00000000
    3: $lr = 111
    4: $r4 = 536870916

在这样的流程里,基本上观察到了启动过程里cpu的行为。

To Be Continued

要学习和记录的内容比较多,先来一篇博文记录最开始的内容,后续会随着阅读的深入对各重点内容加以记录与实验。
感谢阅读~

AAPCS简介

AAPCS(Procedure Call Standard for Arm Architecture),Arm架构下的调用标准,用于描述汇编语言与C代码交互时的行为规范。这里提到的PCS,本质是Arm 架构 ABI(Application Binary Interface)的核心组成。如果目光仅放到Arm-M 系列MCU搭配C语言的开发的话,PCS几乎就是ABI的全部内容。
官方发布页
当前最新的aapcs32

延伸内容

一般来说,每个架构都有自己的PCS。有的时候一个架构可能会有多种调用约定,比如x86 (32位)曾经有cdecl、stdcall、fastcall、pascal 等多种调用约定;而x86-64 (64位)也有System V ABI(Linux、macOS)和Microsoft x64 Calling Convention(Windows)。由于Arm公司的强势与统一,Arm早早定义了全套标准的AAPCS,因此这对开发者更加友好。
C++的ABI兼容是另一个问题,需要单独讨论了。

AAPCS内容

以2025Q4的aapcs32 为例,先看目录,大致看看里面都包含了什么。

Contents
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
Contents
1 Preamble 2
1.1 Abstract 2
1.2 Keywords 2
1.3 Latest release and defects report 2
1.4 Licence 3
1.5 About the license 3
1.6 Contributions 3
1.7 Trademark notice 3
1.8 Copyright 3
2 About This Document 6
2.1 Change Control 6
2.1.1 Current Status and Anticipated Changes 6
2.1.2 Change History 6
2.2 References 8
2.3 Terms and Abbreviations 8
2.4 Acknowledgements 10
3 Scope 11
4 Introduction 12
4.1 Design Goals 12
4.2 Conformance 12
5 Data Types and Alignment 13
5.1 Fundamental Data Types 13
5.1.1 Half-precision Floating Point 13
5.1.2 Containerized Vectors 14
5.2 Endianness and Byte Ordering 14
5.3 Composite Types 15
5.3.1 Aggregates 15
5.3.2 Unions 15
5.3.3 Arrays 15
5.3.4 Bit-fields 15
5.3.5 Homogeneous Aggregates 16
6 The Base Procedure Call Standard 17
6.1 Machine Registers 17
6.1.1 Core registers 17
6.1.2 Co-processor Registers 19
6.2 Processes, Memory and the Stack 19
6.2.1 The Stack 20
6.3 Subroutine Calls 22
6.3.1 Use of IP by the linker 22
6.4 Result Return 23
6.5 Parameter Passing 23
6.6 Interworking 25
7 The Standard Variants 27
7.1 VFP and SIMD vector Register Arguments 27
7.1.1 Mapping between registers and memory format 27
7.1.2 Procedure Calling 27
7.2 Arm Alternative Format Half-precision Floating Point values 28
7.3 Read-Write Position Independence (RWPI) 28
7.4 Variant Compatibility 28
7.4.1 VFP and Base Standard Compatibility 28
7.4.2 RWPI and Base Standard Compatibility 29
7.4.3 VFP and RWPI Standard Compatibility 29
7.4.4 Half-precision Format Compatibility 29
8 Arm C and C++ Language Mappings 30
8.1 Data Types 30
8.1.1 Arithmetic Types 30
8.1.2 Pointer Types 32
8.1.3 Enumerated Types 32
8.1.4 Additional Types 33
8.1.5 Volatile Data Types 33
8.1.6 Structure, Union and Class Layout 33
8.1.7 Bit-fields 33
8.2 Argument Passing Conventions 37
9 APPENDIX: Support for Advanced SIMD Extensions and MVE 38
9.1 Introduction 38
9.2 SIMD vector data types 38
9.2.1 C++ Mangling 40
跳过不感兴趣的部分,不难发现: - 第5章主要描述了数据类型与字节序之类的东西。 - 第6章则主要描述了调用约定,寄存器、参数和返回值之类的。 - 第7章则是一些浮点数与SIMD的东西。 - 第8章是描述C/C++ 语言是怎么映射到底层的手册。

数据类型与对齐

基础数据类型

关于基础数据类型,主要的内容就是

像整形、浮点型、指针这些都算比较常见的概念了,除此之外文档下面还单独提了一嘴半精度浮点数和容器化向量。
Arm架构支持三种半精度浮点数,IEEE754-2008、Arm替代格式、脑浮点数。前两种格式是互斥的,AAPCS的基础格式是IEEE754-2008,然后在调用过程中Arm替代格式也是允许的。
容器化向量的部分有点晦涩,跳过了。

字节序与字节顺序

这部分描述是这样的

本质就是系统寻址的最小单元是字节,但是数据单元可能是由多个字节组成的,这样在表达的时候就会需要规则来约束一下,多个字节组成的数据哪边是高位哪边是低位。

复合数据类型

复合数据类型是一个或者多个基本数据类型组成的集合,而在过程调用中复合数据类型是被认为一个整体。复合类型可能包括:Aggregates(类似struct,成员在内存中顺序排列)、union(成员在内存中重叠排列)、array(成员在内存中重复排列)。
并且,这些定义是递归的;也就是说,每种类型都可以包含一个复合类型作为其成员。

  • 对于复合变量成员的成员对齐,是指应用了任意语言修饰符之后的对齐方式。
  • 对于复合变量成员的自然对齐,是指顶层成员的对齐方式的最大值,也就是对齐调整之前的值。

Aggregates

对于Aggregates,对齐方式应该为对齐度最高的组件的对齐方式。Aggregates实际的大小应该是其对齐方式的最小倍数,该倍数足以容纳其所有成员,前提是这些成员按照这些规则进行布局

Unions

Unions的对齐方式应为其对齐程度最高的成员的对齐方式。联合体的大小应为其对齐方式的最小倍数,该倍数足以容纳其最大的成员。

Arrays

Arrays的对齐方式应为其基类型的对齐方式。数组的大小应为其基类型的大小乘以数组中元素的数量。

Bit-fields

聚合体中作为基本数据类型的成员可以细分为位域;如果此类成员存在未使用的部分,且足以使后续成员以其自然对齐方式开始,则后续成员可以使用未分配的部分。为了计算聚合的对齐方式,成员的类型应为位域所基于的基本数据类型。聚合中位域的布局由相应的语言绑定定义

同构聚合

同构聚合是一种复合类型,其中构成该类型的所有基本数据类型都相同。同构性测试在数据布局完成后进行,并且不考虑访问控制或其他源语言限制。
如果由容器化向量类型组成的聚合的所有成员大小相同,即使容器化成员的内部格式不同,该聚合也被视为同构的。例如,一个包含 8 字节向量和一个 4 个半字向量的结构满足同构聚合的要求。
同构聚合具有一个基本类型,它是每个元素的基本数据类型。总大小是基本类型的大小乘以元素数量;其对齐方式与基本类型的对齐方式相同。

comment

这个部分其实是关于数据和内存的部分,倒是没有发现什么特别需要注意的内容。

基础过程调用标准

寄存器

寄存器又分为核心寄存器与协处理寄存器。

核心寄存器

原始的说法是这样的

有16个32位寄存器r0 - r15,这些寄存器对于汇编是不区分大小写的,但是特定角色的寄存器使用大写的。除了这16个,还有一个状态寄存器CPSR。
对于纯的ASM代码来说,直接操作寄存器就可以,比如像R0-R8 可能地位都是平等的,没有什么特别的。但是一旦涉及到C与ASM的混合编程,就需要一些约定来实现互相的约定与识别。怎么来互联互通,那就是这个core register的用法,其实这里也是我找到这篇文档来阅读的动机了。
逐个过一下:

  • R15(PC):这个是非常核心的寄存器了,它指示了当前程序运行到哪个指令。
  • R14(LR):当前调用结束时,应该返回到哪里继续执行。
  • R13(SP):栈指针,指示当前栈的使用状态
  • R12(IP):过程内调用临时寄存器,用于远距离寻址与编译器临时使用。
  • R4-R11 :寄存器,调用后需要恢复原样(R4-R8, R10, R11(v1-v5, v7-v8):调用后必须保持原样。R9 为 platform-specific,当被定义为 v6 时才需保存)
  • R0-R3 : 寄存器,传递参数与返回值

对于一个典型的调用过程,存在调用方caller 与 被调用方callee。

  • caller 在调用 callee 之前,会将callee 所需要的参数放置到R0-R3(如果参数超过了R0到R3的范围比如传8个参数,那前四个放R0到R3,后面的放栈里),然后LR 填caller 自己(BL的下一条指令),PC 填 callee(类似 BL callee)这样程序的控制权就转移给callee了。
  • callee进入后,它可以按照约定的方式获取到自己所需的参数(R0-R3、栈),然后开展自己所需要的计算或者业务逻辑。如果在callee的业务中需要使用额外的寄存器,callee可以使用其他的寄存器,但是需要保证(R4-R11,SP)的数据被记录下来,在返回caller的时候,要保持原样。所以我们看汇编源码的时候,很多的汇编函数进来就把(R4-R11,SP)push到栈里,然后结束逻辑之后pop回原位就是这个道理。
  • callee在做好现场保存后可以自由的使用R0-R11,SP来完成自己的业务,当运算结束现场也恢复后,通常是要给一个返回值的,而这个返回值一般是放到R0、R1里的。和参数传递类似,返回值传递也会有返回值超过R0、R1的场景,比如说返回值就是一个很大的数据结构,这种情况下R0、R1放不下,SP要保持现场没法往stack里塞,实际是怎么实施的呢?实际中这样的情况是由caller在调用callee之前分配出一块内存,然后将地址放到R0里,callee执行完将result填充一下就可以了。
  • callee完成所有的一切,结束生命周期时,只要设置一下PC = LR,那程序控制权又转给caller了

从典型流程里不难看出,16个寄存器基本上都很忙碌,大家各司其职数据进进出出。但是IP好像和别人都不一样,它不涉及调用的参数传递、返回值传递,也不像PC、LR、SP有单独的职责,那IP是干啥的呢?
IP 的职责有几种:

  • 远距离调用,Arm 的BL指令是有范围的,而超出范围的远距离寻址要依靠寄存器间接寻址。而其它寄存器各司其职都是没法用的,这个时候就可以用IP。
  • 有的编译器会在函数入口做栈溢出检查,比如说,我知道callee中间会用100K stack,那我函数进来的时候就可以比较一下,SP - 100K < stack_limit ? ,这样不就可以知道是不是栈溢出了吗。那这个时候又出现了,其他寄存器各司其职,只有IP无牵无挂,那就用IP来。
  • 总的来说就是,IP是没有职责的草稿纸,谁想用就用,但是也没有任何保证。

另外值得一提的是,R9 的定义是platform-specific的,只有被定为 v6 时才需保存。R4-R8、R10、R11、SP则是无条件保存。

CPSR也有一些自己的规则,这里跳过了。

上面提到的,如果参数或者返回值大于标准流程寄存器所表示的范围,可能会使用栈来传递参数,那么一个64 位的基础数据单元,算不算超过范围呢。
按照AAPCS的说法,对于超过32位的基础数据单元,如果是64位的也就是双字,可以使用两个连续寄存器,比如R0、R1或者R2、R3来传递,可以用一条单独的LDM从内存里取到数据。
对于128位的基础数据单元,也就是containerized vector,可以用4个连续的寄存器来传递,也是一条单独的LDM从内存load。

协处理器寄存器

VFP-v2协处理器有32个单精度寄存器,s0-s31,也可以作为16个双精度寄存器d0-d15访问(其中d0与s0、s1重叠;d1与s2、s3重叠;依此类推)。而其他的实现可能会增加更多的寄存器。比如VFP-v3 增加了16个双精度寄存器d16-d31,但是没有额外的单精度寄存器对应。
高级SIMD扩展和M型向量扩展 (MVE) 使用VFP寄存器集。高级SIMD扩展使用双精度寄存器表示64位向量,并进一步定义四字寄存器(q0与d0、d1重叠;q1与d2、d3重叠;依此类推)用于128位向量。MVE在相同的四字寄存器中使用128位向量。寄存器s16-s31(d8-d15, q4-q7)必须在子程序调用之间保持不变;寄存器s0-s15(d0-d7, q0-q3)无需保持不变(并且可以用于传递参数或在标准过程调用变体中返回结果)。寄存器 d16-d31(q8-q15),如果存在,也无需保持不变。FPSCR和VPR寄存器是唯一可能被符合规范的代码访问的状态寄存器。FPSCR和VPR又有各自的特点,但是跳过这里了。

过程、内存与栈

AAPCS适用于单个执行线程或进程(以下称为进程)。进程具有由底层机器寄存器和其可访问内存内容定义的程序状态。进程在执行过程中可访问的内存(而不会导致运行时错误)可能会有所变化。
一般,进程可以访问五种内存:

  • 代码段,可读但不需要可写
  • 只读静态段,只读
  • 读写静态段,读写

读写静态段,又可被细分为已初始化、零初始化、未初始化。除了栈之外,其他的段都不要求内存单一连续。一个进程必须始终拥有一些代码和一个栈,但是其他类型的内存并非强制的。堆用于分配动态内存,而程序只应该执行代码段中的指令。

stack是一个连续的内存区域,可以用来存储局部变量以及做调用的参数传递。stack是地址递减的,当前使用状态由SP(r13)来表示。通常来说,base 与 limit是用来描述stack的范围,一些时候limit是固定的,也有时候limit可以被动态调整。堆栈的维护规则包括通用规则与公共接口特定规则。
通用规则:

  • Stack-limit ≤ SP ≤ stack-base
  • SP mod 4 = 0
  • 进程只在[SP, stack base - 1] 范围存储数据

类似

1
ldmxx reg, {..., sp, ...} // reg != sp

在遇到中断的时候,就是有可能违反第三条原则的。比如说SP被更改后中断进来,中断进来第一件事就是压栈保存现场,但是这个时候SP就是飞的,压栈就有可能压到别的位置。
公共接口规则:

  • SP mod 8 = 0
    所谓公共接口规则,就是在模块间边界。比如一个.o 暴露给外部可调用的符号。函数内部调用(inline、static)不算。为了更好的兼容性,公共接口要求更严格的对齐。任何时候SP都应该是4字节对齐的,也就是说栈只能4个字节4个字节的用。但是当进程调用的时候,需要把SP做成8字节对齐的。
    实操的时候,尽量把一切按照严格的方法来。比如说,如果你要自己维护一个内存池,那别人从你这里分配内存的时候,你返回的就不能是按照4字节对齐来。
    1
    2
    3
    4
    5
    6
    7
    struct Foo {
    double a; // 需要 8 字节对齐
    int b;
    };
    struct Foo *f = my_malloc(sizeof(struct Foo));
    f->a = 3.14;
    // 如果 my_malloc 返回 0x1004,a 的 offset = 0,但 0x1004 % 8 != 0 -> fault。因为f->a 可能用了STRD,而STRD无条件要求地址与传输大小对齐。
    关于栈探测,是为了防止悄悄爆栈所以在申请大的stack空间时,先逐页读一下,比如说应用申请1M的stack,那就拆成4K的步长,依次步进读一个字节,这样就可以避免静默的溢出。
    关于FP,也就是Frame Pointer,是用于调试和回溯的工具。当你想知道当前的程序是怎么一层层调进来的时候,FP可以帮助到你。每层调用在栈上放一个Frame Record(2个word),分别保存LR与前一个FP,这样就形成了一个调用链,而当前一个FP为0,调用链结束。

子调用

Arm 和 Thumb指令集都有一个BL指令,它就是将当前的BL的下一条指令放进LR,将callee放进PC。如果BL指令是Thumb的,那LR的首位将被置为1,Arm则为0.

链接器使用IP

Arm 和 Thumb的BL均无法实现完整的32位寻址,因此需要linker在caller 和 callee之间插入一个veneer。而插入的veneer需要保留除IP(r12)和条件码之外的所有寄存器内容。

返回值

返回值返回的方式取决于返回值的类型。

  • 半精度浮点返回值(16位),会被返回到r0的LSB
  • 小于4字节的基础数据类型会被返回到R0(零扩展或者符号扩展到一个字)
  • 刚好4字节的基础数据类型会被返回到R0
  • 双字也就是8字节的基础数据类型会被返回到R0与R1
  • 128位也就是16字节的基础数据类型会被返回到R0到R3
  • 不超过4字节的复合数据类型会返回到R0,其格式如该结果存储在字对齐的地址然后通过LDR读取到R0一致。R0中其它位的值为未指定的
  • 大于4字节的复合数据类型,或者其大小无法由caller、callee静态确定的类型,将在内存中做返回值传递。该内存作为一个额外参数在调用时传递,并由callee在调用过程中修改。

参数传递

基本标准规定了在核心寄存器(r0-r3)和堆栈上传递参数。对于只接受少量参数的子程序,仅使用寄存器,从而大大减少调用开销。
参数传递被定义为一个两级的概念模型:

  • 将源语言的参数映射到机器类型
  • 将机器类型整理成最终的参数列表
    从源语言到机器类型的映射对于每种语言都是固定的,并且是单独描述的。最终的结果将会是一个有序的参数列表。
    后面有一些非常细节的参数处理过程,跳过了。

comment

这一章节其实是比较核心的内容,对典型场景的行为模式做了规范。

Ending

AAPCS 整体上还是比较底层视角的描述,有很多的细节是应用开发者所无需了解的。但是,尤其是在深度的问题排查时,来自AAPCS的一些说明可以帮助我们更好的理解汇编源码。小册子本身也不长,读完至少能看懂反汇编里的出入栈、参数传递、返回值的处理。

网上冲浪多了,经常看到很好的网站和博客,有时候欣赏一会就放进浏览器收藏夹吃灰了。转念一想,不如找个地方挂出来晒一晒,利己利他。

ruanx.net

这里是一个很好的博客,最开始是搜索嵌入式相关的内容发现的。后来发现博主的其他领域内容也很好,读了下都很受益。博主可能比我还小两岁,汗颜哇。

lujji.github.io/blog/

这里是一个嵌入式相关的博客,我是被stm8相关bare mental编程的内容吸引过来的,博主的工作做的很扎实,偶尔贴的图也工工整整、赏心悦目。就是停更许久了。

zhailog.com/

这里是一个比较关注NFC的博主个站,我是搞proxmark3从B站发现的,那段时间对NFC的知识非常急需,从这个博主这里学了一些。但是需要指出,这个博主的方向其实更偏向日常的NFC卡片爱好者,就是告诉你当前爱好者主流工具有哪些,然后教你怎么用的。如果你需要一些用于商业用途的开发或者是产品,这个可以带你很好的入门。博主的图做的嘎嘎好

vibevibe.cn/

这里是一个关于vibe coding的可以说是布道网站吧,我觉得这个网站比较适合分享给对ai coding有兴趣但是没有coding经验的同学来启蒙,同时自行摸索的同学可以从这里刷一遍名词解释,中文网站阅读友好。

csdiy.wiki/

在AI显学的时代,如果你还要学一些古典的手工编码,对某个领域或者编程语言感兴趣,这里算是有一个小小的路书,帮你指下路。

xuanxuanblingbling.github.io/

实话说,有点忘记怎么翻到这个博客了,回顾了一下应该是偏安全方向的了,可能是之前做密码学的时候搜到的,他的博客名特别又好记,就一直有这个记忆。同样停更一段时间了

ruanyifeng.com/blog/

阮老师的博客那是不必多言了,有口皆碑了。前AI时代,如果你想学什么东西刚好阮老师有文章写了的话,那这篇文章大概率是质量远超CSDN的。然后科技周报栏目也不错,可以看看新鲜事。主要是一件事做的久了,自然就形成了口碑,这就是品牌的力量。

安富莱嵌入式周报

嵌入式方向的新鲜事汇总,也是坚持了很多期。可以看看别人都用嵌入式做了什么有意思的事情,有的时候灵感真的是比技术重要的,都看看别人在干嘛,不要老自己发呆。

hackaday.com/

国外的硬件骇客论坛,很多新鲜玩意儿。

beginners.re/

关于汇编的好东西

hacker news

权威,无需多言

oshwhub.com/

立创搞的硬件开源平台,实话说逼格不如hackaday,但是里面也有很多大佬。我愿意称之为更适合中国宝宝国情的硬件开源平台。

qbitai.com/

用于看LLM这一轮爆发的各种八卦的,不对,也算是行业进展吧。程序员可能是最先关注也最深刻被AI影响的行业,我们静观其变。

latepost.com/

这里主要是关注明星企业的行业故事了,包括新能源、互联网、大模型,增长见闻吧。

BTFZ SDR

关于SDR的一些教程,SDR本身是小众领域,在这个方向高质量的中文教程更加难得。

怎么提交博客

当前部署的hexo,分mian 和 source两个分支。往source文件夹下写markdown,推到云端后会自动触发流水线渲染到main分支。

1
2
3
4
5
6
7
touch source/_posts/xxx.md
# write something
hexo s
# feel ok
git add source/_posts/xxx.md
git commit -m "xxxx"
git push origin source

超链接语法和原生markdown没区别

1
[github](https://github.com)

但是如果贴图的话,用Post Asset Folder + Hexo的img语法稳一点,类似

1
{% asset_img xxx.png %}

需要配置_config.yml 中的post_asset_folder为true,同时将png放到md的同名目录下。

0%