没志青年
发布于 2025-10-10 / 59 阅读
0

调试技巧[02] - Cortex-M 基础概念

Cortex-M3/4 关键特点:

  • 哈佛架构

  • 使用 NVIC 管理中断

  • 32 位寻址,支持 4GB 空间。

  • Thumb-16/32 指令

寄存器

通用寄存器R0-R12

R13:SP

R14:LR

R15:PC

xPSR 状态寄存器

R13 堆栈指针:同一时刻只能有一个生效

  • 进程堆栈指针PSP:任务使用

  • 主堆栈指针MSP:中断、异常使用

指令集

ARM 指令集

Thumb 指令集

一个新的项目默认是空白的:

从编译结果反汇编:

fromelf --text -a -c --output=编译输出目录\自定义文件名.dis    译输出目录\可执行文件名称.axf
fromelf --text -a -c --output="$L@L.dis" "#L"

编译输出目录可执行文件名称不是随便写的,是项目中配置的,要对应上,否则找不到文件。

因此这里是:fromelf --text -a -c --output=ceu6_test\ceu6_test.dis ceu6_test\ceu6_test.axf

Flash 中保存的是机器码。

从 Keil 中打开项目所在的文件夹:

随便打开文件,然后文件名上右键:

反汇编也可以用:

fromelf --text -c -o "$L@L.asm" "#L"

生成.asm文件,这种比较精简,没.dis文件丰富。??加上-a不和.dis一样了吗,待验证。

常用汇编指令

赋值

MOV R0,#666

R0 寄存器的值设置为

MOVS

读写 LDR、STR

LDR 用于读,STR 用于写。

这样记: Load 加载,Store 存储,后面有个R表示寄存器,即读写寄存器。

数据传输三要素:源 目标 长度

LDR 数据是向左流动的 <---

MOV r1, #0x20000000
LDR r0, [r1]
LDR R0,[R1, #2]  ; 源的地址就是R1寄存器中的地址 + 2

LDR 伪指令

STR 数据是向右流动的 -->

STR 

同样STR也有伪指令

长度体现在指令上,LDR、STR 的单位都是 4 个字节,LDRH、STRH 表示读写 2 个字节,LDRB、STRB 表示读写一个字节。

LDRD double,即一次性操作两个 LDR

汇编中一个函数的写法:


rw_ram  PROC    ; 标记函数开始
        EXPORT  rw_ram             [WEAK]   ; 导出函数,这样 C 语言中才能访问
        MOV r1, #0x20000000
        LDR r0, [r1]
        BX LR
        ENDP    ; 标记函数结束

验证一下:

验证 LDRB:

由于 Cortex-M3 是小端序,因此读的是最低地址的字节。

验证STR:

写入的是4个字节,默认是0,使用STRB就不会影响其它字节了。

出入栈

入栈就是

出栈就是

要成对出现。

加减法

比较 CMP

按位与、或运算

跳转 BL

跳转命令 修改PC寄存器,跳转到其它地方执行。

BL:1、将下一条指令的地址赋值到LR寄存器,并且bit0设置为1,表示是Thrumb指令 2、跳转

B、BL 后面其实不是完整的地址,只是方便开发人员调试,观看。

其实是偏移量,因为存不了完整的地址。

BL 用于函数之间这种需要返回的跳转

B 用于跳转,不需要返回,因此用不到LR寄存器

BX R0 跳到 R0 所表示的那个地址。

BLX RO 1、让LR=下一跳指令的地址 2、跳转到R0表示的地址

这个视频中也讲了MOV不能是特殊的数的原因

伪指令,不用我们自己去手动定义了。

函数调用过程

ARM架构过程调用标准 AAPCS:

  1. 使用 R0-R3 传递参数和返回值,返回值使用R0传递

  2. C函数执行前后,R4-R11 的值不会被修改

函数调用行为:

  • 调用者:由于 R0-R3 寄存器很可能被被调用者修改,所以标准要求R0-R3 由调用者负责保存,还有 R12、S0-S15。

  • 被调用者:R4-R11 被调用者一般用不到的,所以标准规定了由被调用者保存,如果用到了,需要入栈保存,结束时出栈,其他的还有 S16-S31

最大支持n个参数

在main函数中调用 add 函数

int Add(volatile int a, volatile int b)
{
    volatile int sum;
    sum = a + b;
    return sum;
}

void main() {
    ...
    volatile int sum_main = Add(100, 1);
    ...
}

分析汇编代码:

可以看出寄存器顺序是参数从左到右依次增大的

i.Add
    Add
        0x080002f4:    b503        ..      PUSH     {r0,r1,lr}     ; 入栈,保存参数、跳转地址
        0x080002f6:    b081        ..      SUB      sp,sp,#4       ; 栈指针减4,分配空间
        0x080002f8:    e9dd0101    ....    LDRD     r0,r1,[sp,#4]  ; 读参数到寄存器中
        0x080002fc:    4408        .D      ADD      r0,r0,r1       ; 加法运算
        0x080002fe:    9000        ..      STR      r0,[sp,#0]     ; 返回值存入R0
        0x08000300:    bd0e        ..      POP      {r1-r3,pc}     ; 出栈,LR赋值给PC进行跳转,这里R1-R3没有实际的意义,只是用于占位

堆栈的变化

void BB(volatile int d, volatile char val)
{
    volatile char buf[101];
    buf[d] = val;
}

    i.BB
    BB
        0x08000302:    b503        ..      PUSH     {r0,r1,lr}
        0x08000304:    b09a        ..      SUB      sp,sp,#0x68     ; 分配内存就是减堆栈指针,因为要对齐所以不是0x65
        0x08000306:    f89d006c    ..l.    LDRB     r0,[sp,#0x6c]
        0x0800030a:    9a1a        ..      LDR      r2,[sp,#0x68]
        0x0800030c:    f80d0002    ....    STRB     r0,[sp,r2]
        0x08000310:    b01c        ..      ADD      sp,sp,#0x70
        0x08000312:    bd00        ..      POP      {pc}

内存对齐到4的整数

    i.CC
    CC
        0x08000316:    b508        ..      PUSH     {r3,lr}    ; R3可以直接用,但是这里入栈,是为了变量分配空间
        0x08000318:    4408        .D      ADD      r0,r0,r1
        0x0800031a:    4410        .D      ADD      r0,r0,r2
        0x0800031c:    4418        .D      ADD      r0,r0,r3
        0x0800031e:    9000        ..      STR      r0,[sp,#0]
        0x08000320:    bd08        ..      POP      {r3,pc}