# ARM 汇编基础知识

# 汇编

大部分做嵌入式开发时候,使用的都是 C 语言,通过编译器生成二进制文件,然后烧录到 MCU 中.
其实,更底层还有汇编语言机器码,例如.start_up.s 文件就是汇编文件,用于设置堆栈,中断向量表以及中断向量函等
不同的芯片指令集可能不同,所对应的机器码也会不同,汇编的出现就是为了帮助记忆
同一条汇编代码,由汇编器来实现转译

# 反汇编

反汇编是将难以辨认的机器转化为汇编语言,反汇编出来的汇编大概率和原本的汇编代码不一样
例如,原汇编内容如下
image
反汇编后,引导文件对应的代码如下
image
可以看到有着很大的差别,但是,毫无疑问二者都可以实现最终效果

# 二进制文件生成流程

正常来说,一个 C 语言程序,会先经过预处理生成 x.i 文件,这时候还是 C 语言文件,仅仅是完成了将头文件内容放入 x.c 的文件夹中
接下去就是编译,将 x.i 文件编译为汇编文件 x.s , 这时候得出来的文件就是汇编文件了
再接下去就是将各个 x.s 文件汇编为 x.o 文件,这时候文件就是二进制格式了,无法打开查看了
最后通过链接器将 x.o 文件,合并成 x.elf 文件用于下载
这时候如果想看汇编的话就需要通过反汇编器来实现反汇编 x.dis , 一般来说各种编译器自带了反汇编器
image

# KEIL 下怎么反汇编

在 KEIL 的 User 选项中,如下图添加这两项 (filename 自行替换)

fromelf  --bin  --output=led.bin  Objects\filename.axf
fromelf  --text  -a -c  --output=filename.dis  Objects\filename.axf

然后重新编译,即可得到二进制文件 filename.bin、反汇编文件 filename.dis。
如下图操作:
image

# GCC 下反汇编

使用 GCC 工具链编译程序时,可以在 Makefile 中添加一句:

$(OBJDUMP) -D -m arm  filename.elf > filename.dis	# OBJDUMP = arm-linux-gnueabihf-objdump
``
就可以把可执行程序 filename.elf,反汇编,得到 filename.dis

# PC 寄存器

PC 寄存器默认指向当前指令的下一条指令位置
但是,不同架构的处理步长会不同,Thumb 指令集中 PC 步长为 4, ARM 指令集中 PC 步长为 8, 而 Thumb2 指令集为了兼容 Thumb 指令集就使用了 4 作为步长
image

# 汇编如何调用 C 语言函数

# 直接调用

如果函数调用不涉及参数传递,那么可以直接调用

例如,调用 void SystemReset(void) , 那么就可以直接调用

bl SystemReset

如果函数具有返回值,例如 uint8_t is_empty(void); , 当调用的时候可以直接调用
返回值会存入 r0~r3

bl is_empty
and r0, #0xff

# 含有参数的函数

如果函数带有参数,那就涉及到了传参的问题,怎么传,用什么传?

其实 ARM 中有一个 ATPCS 规则 ARM-THUMB procedure call standard (ARM-Thumb 过程调用标准)
这个标准的第 10 页约定 r0~r15 寄存器的用途

  • R0~r3: 用于参数传递和数值返回
  • r4~r11: 用于保存当前函数的局部变量

008_atpcs

例如,调下面这个函数

int delay(uint32_t n){
  while (n--);
  return 0;
}

在汇编中的调用代码为

ldr r0, =0x7fffff ; 传递参数, 存放到r0
bl  delay
cmp r0, #0 ; 校验返回值

思考一个问题当参数超级多的时候会怎么样?
当函数参数过多的时候,会使用栈来保存

r4~r11 作为局部变量的保存点,可能在调用时会发生改变
这是因为这些寄存器时共享的,CPU 的任何运算操作都需要借助寄存器来实现
所以在调用函数时,需要将 r4~r15 以及 psr 寄存器都保存下来 (当然因地制宜,没用到就不存了,要不然浪费空间)

# C 函数的汇编代码

这里是一段 STM32MP157 的 led 点亮代码,由于 main 函数太长这里只分析一下 delay 的调用和执行

#define USE_A7
static void delay(volatile long ntime)
{
    while (ntime--)
    {
        ;
    }
}
int main(void)
{
    volatile unsigned int *p_reg;
    /* 使能 PLL4 */
    p_reg = (volatile unsigned int *)(0x50000000 + 0x894);
    *p_reg |= (1 << 0);
    // 等待时钟稳定
    while (!(*p_reg & (1 << 1)))
    {
        ;
    }
    /* 使能 GPIOA */
#ifdef USE_A7
    p_reg = (volatile unsigned int *)(0x50000000 + 0xA28);
    *p_reg |= (1 << 0);
#endif
#ifdef USE_M4
    p_reg = (volatile unsigned int *)(0x50000000 + 0xAA8);
    *p_reg |= (1 << 0);
#endif
    /* 设置 GPIO10 为输出模式 */
    p_reg = (volatile unsigned int *)(0x50002000 + 0x00);
    *p_reg &= ~(3 << 20);
    *p_reg |= (1 << 20);
    // 指向 (GPIOA_ODR)
    p_reg = (volatile unsigned int *)(0x50002000 + 0x14);
    /* 执行任务 */
    while (1)
    {
        /* 设置 LED 电平变化 */
        *p_reg ^= (1 << 10);
        delay(1000000);
    }
    return 0;
}

经过反汇编后,这里就只给出调用和 delay 的代码,简单进行一下分析


c01000bc:	f244 2040 	movw	r0, #16960	; 0x4240 ; 将0x4240存入r0的低16位
c01000c0:	f2c0 000f 	movt	r0, #15              ; 将0x0f存入r0 的搞16位
; 上面的出来的就是100000
c01000c4:	f7ff ffa2 	bl	c010000c <delay>       ; 调用 delay 函数

c010000c <delay>:
c010000c:	b480      	push	{r7}                 ; 保存共享过程寄存器的原有的值
c010000e:	b083      	sub	sp, #12                ; sp向后偏移12Byte
c0100010:	af00      	add	r7, sp, #0             ; 将sp的值读取如r7
c0100012:	6078      	str	r0, [r7, #4]           ;  r0 存入到 sp+4 的地址
c0100014:	bf00      	nop
c0100016:	687b      	ldr	r3, [r7, #4]           ; 将sp+4 地址上的数据读入到r3
c0100018:	1e5a      	subs	r2, r3, #1           ; 将 r3 的值-1 ntime-1
c010001a:	607a      	str	r2, [r7, #4]           ; 更新SP+3的数值
c010001c:	2b00      	cmp	r3, #0                 ; 比较是否减到0
c010001e:	d1fa      	bne.n	c0100016 <delay+0xa> ; 是则返回
c0100020:	bf00      	nop
c0100022:	370c      	adds	r7, #12              ; 出栈
c0100024:	46bd      	mov	sp, r7                 ; 回复 sp 数值
c0100026:	f85d 7b04 	ldr.w	r7, [sp], #4         ; 回复 r7
c010002a:	4770      	bx	lr

图片简单讲解一下,更详细的可以参考韦东山的视频
image

# 使用汇编实现点灯

分析和了解了这么多,总得实战一下,其实本质上是一个 读 - 改 - 存 的过程
一般来说,当出现清零,或者亦或之类的运算在 改 这个步骤上会多一些
; 不是注释,这里的注释符号和 C 语言一致

.text
.global  _start

_start: 	

    /* 1. 使能GPIO */

    /* 使能 RCC */
    ldr r0, =(0x50000000 + 0x894)
    ldr r1, [r0]
    orr r1, r1, #(1<<0)
    str r1, [r0]
 
NotReady:
    ldr R1, [R0]
    tst r1, #(1 << 1)
    beq NotReady   /* 不为0则等待 */

   /* 使能 GPIOA 模块 */
    ldr r0, =(0x50000000 + 0xA28)
    ldr r1, [r0]
    orr r1, r1, #(1<<0)
    str r1, [r0]

    /* 配置GPIO引脚 */

    /* 设置GPIO10为输出模式 */
    ldr r0, = (0x50002000 + 0x00)
    ldr r1, [r0]
    mov r2, #(3<<20)      /* 用于生成清除位 */
    bic r1, r1, r2        /* r1 = r1&~(R2) */
    orr r1, r1, #(1<<20)  /* 配置为 输出 */
    str r1, [r0]

    /* r0 指向 (GPIOA_ODR) */
    ldr r2, = (0x50002000 + 0x14)

Loop:

    /* 设置引脚高电平 */
    ldr r1, [r2]
    orr r1, r1, #(1<<10)
    str r1, [r2]
    
    ldr r0, =1000000
    bl Delay

    /* 设置引脚低电平 */
    ldr r1, [r2]
    bic r1, r1, #(1<<10)
    str r1, [r2]
    
    ldr r0, =1000000
    bl Delay
    
    b Loop


Delay:
    subs r0, r0, #1
    bne Delay
    mov pc, lr

能明显的感觉到,咱们的代码要比反汇编的短,会更加高效一些
这是因为编译器会在保证稳妥的情况下再选择高效的指令

当然,还会有更加高效的算法,但这是我能力内目前能想到的一个方案
如果有更好的方法,望各位大佬斧正

# 参考文献

[1] ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition
[2] DDI0403E_B_armv7m_arm
[3] PM0056
[4] ATPCS (ATM-Thumb 指令调用标准)


大道五十,天衍四十九,人遁其一!

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

黑羊 支付宝

支付宝