ARM嵌入式学习(七)--- 汇编基础(数据指令、函数调用原理、中断原理)

目录

一、C语言基础

1.数据类型

2.变量类型

补充知识点:

二、汇编数据处理指令

[🔹 Rd ------ 目标寄存器 (Destination Register)](#🔹 Rd —— 目标寄存器 (Destination Register))

[🔹 Rn ------ 第一操作数寄存器 (First Operand Register)](#🔹 Rn —— 第一操作数寄存器 (First Operand Register))

[🔹 Operand2 ------ 第二操作数 (Second Operand)](#🔹 Operand2 —— 第二操作数 (Second Operand))

1.ADD/ADC:

2.SUB

3.AND/ORR/EOR

5.BIC

6.MOV/MVN/LDR

7.CMP

8.B

三、函数的调用原理

[1. ARM 的堆栈类型](#1. ARM 的堆栈类型)

[2. STMFD 与 LDMFD 指令](#2. STMFD 与 LDMFD 指令)

[2.1 STMFD ------ 入栈(Push)](#2.1 STMFD —— 入栈(Push))

[2.2 LDMFD ------ 出栈(Pop)](#2.2 LDMFD —— 出栈(Pop))

[3. 感叹号(!)的作用](#3. 感叹号(!)的作用)

[4. 初始化栈sp寄存器(满递减栈):](#4. 初始化栈sp寄存器(满递减栈):)

5.栈使用(函数调用原理)

(1)调用原理(栈SP寄存器):

(2)参数传递:

(3)跨文件汇编和c语言于、汇编混合编译:

汇编文件需要用到C语言的函数:

c语言文件要用汇编的函数:

C跟汇编混合编程时需要字节对齐,头部要加preserve8

四、更改工作模式

五、中断原理(异常处理)

[1.使用swi 软中断 ,会进入异常向量表](#1.使用swi 软中断 ,会进入异常向量表)

2.中断函数:

[2.1 计算 SWI 指令的地址](#2.1 计算 SWI 指令的地址)

[2.2 读取 SWI 指令](#2.2 读取 SWI 指令)

[2.3 提取 24 位软件中断号](#2.3 提取 24 位软件中断号)

[2.4 恢复现场并返回(^ )的作用](#2.4 恢复现场并返回(^ )的作用)

六、补充


一、C语言基础

讲解汇编原理之前,用一副图回顾一下C语言基础

1.数据类型

2.变量类型

要记住指针和基类型这两个的定义

这里列举一些例子,判断是什么变量类型只需要看最后的两个字是什么。

补充知识点:

定义一个变量,如果是全局的变量则不需要初始化为0,默认就是0

如果是局部变量则需要初始化为0,不然就是随机值

二、汇编数据处理指令

语法解释:

🔹 Rd ------ 目标寄存器 (Destination Register)
  • 含义 :存放指令运算结果的寄存器。

  • 特点:通常是第一个参数。

  • 例子ADD R0, R1, R2 中的 R0 就是 Rd。意思是"把运算结果保存在 R0 里"。

🔹 Rn ------ 第一操作数寄存器 (First Operand Register)
  • 含义:存放第一个参加运算的数值的寄存器。

  • 特点 :它是源操作数,是运算的基础数据。

  • 例子ADD R0, R1, R2 中的 R1 就是 Rn。意思是"用 R1 的值加上别的数"。

🔹 Operand2 ------ 第二操作数 (Second Operand)
  • 含义 :第二个参与运算的数据。它非常灵活,这也就是图片里提到的"桶型移位器"的威力所在。

  • 特点 :它不一定是 一个固定的数字,也不一定只是一个寄存器,它可以是一个经过移位处理的寄存器值

  • 三种形式

    1. 立即数 :一个常数。例如 ADD R0, R1, #5 中的 #5 。(**指令中自带的常量,**读取速度最快,因为CPU执行到这条指令时,数据已经"在手边"了)

    2. 寄存器 :直接使用另一个寄存器的值。例如 ADD R0, R1, R2 中的 R2

    3. 移位寄存器 :先把一个寄存器的值进行移位(左移、右移等),然后再参与运算。这是 ARM 的特色。例如 ADD R0, R1, R2, LSL #2,意思是先把 R2 的值左移2位(相当于乘以4),再加上 R1 的值,最后存到 R0。

1.ADD/ADC:

ADD(不更新标志位),以及ADDS(更新标志位但不进位)

add r0, r0, r1 ;汇编中的注释用;号表示,

;把r1和r0的值加起来然后存到r0寄存器中

ADC(进位,CPSR的进位C会更新)

如果是32位的寄存器要计算0x1 ffff ffff 和0x 04的加法运算:

bash 复制代码
mov r0 #0x1
mov r1,#0xffffffff
mov r2,#0
mov r3,#4
adds r5, r1, r3
adc r4, r0, r2

这里ADDS指令执行后,CPSR中的进位C更新了,然后ADC指令会读取CPSR中的进位C加到当前的寄存器中

2.SUB

和add一样,减法运算,也有subs和sbc

3.AND/ORR/EOR

and按位与:

对 Rn 和 Operand2 的每一位进行与运算,结果存到rd中

and rd,rn,operand2

orr按位或

对 Rn 和 Operand2 的每一位进行或运算,结果存到rd中

orr rd,rn,operand2

eor按位异或

对 Rn 和 Operand2 的每一位进行异或运算,结果存到rd中

eor rd,rn,operand2

5.BIC

效果:按位清除,用 Operand2 中为1的位去清除 Rn 中对应的位(将那些位强制变为0)

bic rd, rn, #0x0F → 将 rn 的低4位清零(无论原来是0还是1),其余位保持不变,结果存入 rd。

6.MOV/MVN/LDR

指令 核心作用 示例
MOV 将数据原样复制到目标寄存器 MOV R0, #0x0F → R0的值变为 0x0F
MVN 将数据按位取反(Bitwise NOT) 后再传送到目标寄存器 MVN R0, #0x0F → R0的值变为 0xFFFFFFF0 (即~0x0F)
LDR 加载任意 32 位立即数 ldr sp, =0x40001000

arm中MOV可以加载0x0-0xFF范围的立即数,但超过这个范围就需要一些规则来存,比如,MOV指令不能直接加载像0x40001000这样的大数值,这是因为ARM的MOV指令对立即数有严格的编码限制。一旦你尝试使用超出范围的立即数,汇编器就会报错(报警)。而LDR伪指令则专门用来解决这个问题。

7.CMP

cmp比较(结果放在CPSR,所以c语言中if的条件判断结果都是放在CPSR中)

通过cmp加上条件码,我们就可以实现c语言的if语句和switch语句

比如我想判断r0和r1的大小然后把数据填入r2中,把条件码写到mov后即可:

如果是多个条件怎么实现呢,结果是再cmp中嵌套cmp,举个例子(这个例子的代码逻辑有问题,但能看懂if的多条件在汇编中怎么实现的就行)

8.B

类似于c语言中的goto

用这个可以实现函数的调用和c语言中的while语句

b后面也可以跟条件码,这是一个1-100的累加,加到第100个数跳出循环

三、函数的调用原理

调用函数,就必须了解arm中的SP寄存器

在 ARM 汇编中,入栈出栈 通常使用 STM (Store Multiple)和 LDM (Load Multiple)指令,配合 FD (Full Descending)后缀,再加上一个 感叹号(!) 来控制堆栈指针(SP)的更新。下面我们详细拆解。


1. ARM 的堆栈类型

ARM 支持多种堆栈模式,但嵌入式开发中最常用的是 满递减堆栈(FD, Full Descending)

  • :SP 指向最后压入的数据(即栈顶)。

  • 递减 :压栈时 SP 向低地址移动(先减后存),出栈时向高地址移动(先取后加)。

因此,ARM 专门设计了 STMFDLDMFD 指令来完美匹配这种堆栈操作。


2. STMFD 与 LDMFD 指令

2.1 STMFD ------ 入栈(Push)

  • 语法STMFD SP!, {寄存器列表}

  • 功能 :将寄存器列表中的内容按顺序压入堆栈,并更新 SP

  • 实际动作(满递减栈):

    1. 从高地址到低地址依次将寄存器存入 SP 指向的地址。

    2. 每次存一个寄存器前,SP 先减 4(因为 ARM 是 32 位,一个寄存器占 4 字节)。

    3. 存入后 SP 指向新的栈顶。

    4. 感叹号表示将最终 SP 的值写回 SP 寄存器。

2.2 LDMFD ------ 出栈(Pop)

  • 语法LDMFD SP!, {寄存器列表}

  • 功能 :将堆栈中的数据弹出到寄存器列表,并更新 SP

  • 实际动作(满递减栈):

    1. 从当前 SP 指向的地址读取数据到寄存器。

    2. 每读一个寄存器,SP 加 4。

    3. 全部读完,SP 指向新的栈顶(即原来栈顶之上的位置)。

    4. 感叹号同样表示更新 SP。


3. 感叹号(!)的作用

感叹号 称为 "写回" (Write-back)标志,表示在完成数据传输后,将计算出的最终地址写回基址寄存器(这里就是 SP)。

如果没有 !,则 SP 在入栈或出栈后不会自动改变 ,这在需要重复使用同一块栈空间时有用,但堆栈操作通常必须更新 SP,所以 ! 是必不可少的。

例如:

  • STMFD SP, {R0-R3} → 将寄存器压栈,但 SP 不变(危险,下次压栈会覆盖)。

  • STMFD SP!, {R0-R3} → 压栈后 SP 自动向下移动 4×4 = 16 字节,指向新的栈顶。

4. 初始化栈sp寄存器(满递减栈):

ldr sp, =0x40001000(SP通常要求8字节对齐(AAPCS标准))

栈底为 0x40000000,那么栈空间大小就是 0x1000 字节

5.栈使用(函数调用原理)

(1)调用原理(栈SP寄存器):

进入函数后保存当前状态:入栈r4-r12的值以及lr

函数执行完毕后恢复状态:出栈r4-r12的值,以及把lr的值出栈到pc上

(2)参数传递:

ARM 调用约定(ATPCS) 规定:

前 4 个参数通过 R0 ~ R3 传递。

第 5 个及之后的参数通过栈传递,并且由调用者负责在调用前将参数压栈,调用后负责恢复栈指针(一般由调用者平衡栈)。

如:

总结超过四个参数后,调用函数会自动往栈上去找值

(3)跨文件汇编和c语言于、汇编混合编译:

汇编文件需要用到C语言的函数:

要在汇编文件里面导入:

import c_add(函数名)

c语言文件要用汇编的函数:

首先在汇编文件导出

export asm_add(函数名)

然后在c文件中声明这个函数再调用:

int asm_add(int x, int y);

这个函数声明你随便任意类型都行,甚至多个参数都可以,但是实现的功能只能是汇编中写 的

C跟汇编混合编程时需要字节对齐,头部要加preserve8

四、更改工作模式

使用MRS和MSR指令

_fields是域:即图中的四块区域f,s,x,c

注意不同的工作模式下SP是独立的,所以进入USR模式后要把sp寄存器的值重新给一下

复制代码
start							 ;主程序 
	ldr sp, =0x40001000			 ;初始化栈指针
	mrs r0, cpsr				  ;将当前程序状态寄存器(CPSR)的值读取到通用寄存器 r0
	bic r0, r0, #0x1f			   ;将 r0 的低 5 位清零(0x1F = 31)。低 5 位是 CPSR 的模式位(M[4:0]),这一步是清除当前模式
	orr r0, r0, #0x10			   ;将 r0 的低 5 位设置为 10000(二进制),即 用户模式(User Mode)的编码。
	msr cpsr_c, r0					;将 r0 的值写入 CPSR 的控制域(c 表示控制域,包含模式位、中断使能位等)
    ldr sp, =0x40000c00			   ;为用户模式设置一个新的栈指针 sp

示例代码中工作模式为USER

五、中断原理(异常处理)

1.使用swi 软中断 ,会进入异常向量表

swi可以带入参数如:swi #3、swi #7

这里要说明一下,进入向量表后,PC会跳转到相应的地址:

所以在b start的后面不能有任何其他无关指令,修改示例为(nop为任意函数或指令):

bash 复制代码
	preserve8
	area reset, code, readonly
	code32
	entry

	b start   	;reset这里是0x00
	nop			;undef这里是0x04,依次往下4个字节
	b deal_swi 	;swi
	nop			;pre abort
	nop			;data abort
	nop			;reserved
	nop			;irq
	nop			;fiq

start
	ldr sp, =0x40001000	
	mrs r0, cpsr	
	bic r0, r0, #0x1f
	orr r0, r0, #0x10
	msr cpsr_c, r0
    ldr sp, =0x40000c00

2.中断函数:

这里的swi我传入了参数7,进入函数后会处理这个SWI

swi的中断向量表位置我们使用函数来表达,在里面处理这个数据把参数提取出来(因为数据和指令混合在一起的)

bash 复制代码
deal_swi
    stmfd sp!, {r4-r12, lr}          ; 保存现场
    sub r0, lr, #4                   ; 计算 SWI 指令的地址
    ldr r1, [r0]                     ; 读取 SWI 指令本身
    bic r0, r1, #(0xff << 24)        ; 提取低 24 位(SWI 号)
    import c_deal_swi                ; 声明外部 C 函数
    bl c_deal_swi                    ; 调用 C 函数,参数在 r0 中
    ldmfd sp!, {r4-r12, pc}^         ; 恢复现场并返回,同时切换模式

我们详细讲解这个函数是如何提取swi的参数的:

2.1 计算 SWI 指令的地址

复制代码
sub r0, lr, #4
  • 当 SWI 异常发生时,处理器会自动将 返回地址 存入当前模式的 lr 寄存器中。对于 SWI 异常,lr 中存放的是 SWI 指令的下一条指令地址

  • 为了获得 SWI 指令本身,只需要将 lr 减去 4 即可,因为 ARM 指令长度固定为 4 字节。

  • 执行后,r0 中保存了 SWI 指令的内存地址。

2.2 读取 SWI 指令

复制代码
ldr r1, [r0]
  • r0 指向的内存地址(即 SWI 指令所在位置)读取一个 32 位的指令字,存入 r1

2.3 提取 24 位软件中断号

复制代码
bic r0, r1, #(0xff << 24)

最后r0寄存器就存入了我们的中断号7,这时候再设计一个switch函数来判断中断号并执行相对于的语句就是我们熟悉的c语言中的中断服务函数了

2.4 恢复现场并返回(^ )的作用

复制代码
ldmfd sp!, {r4-r12, pc}^
  • ^ 后缀 的作用:

    • pc 在寄存器列表中且使用 ^ 时,除了从堆栈中恢复 pc(即返回地址)外,还会将当前模式的 SPSR 自动恢复到 CPSR(上一篇文章的异常处理过程)

    • 这实现了从异常模式(如 SVC 模式)返回到中断发生前的模式(如用户模式或系统模式),同时恢复处理器状态标志、中断使能位等。

  • 如果没有 ^,则仅仅将 pc 弹出,但不恢复 CPSR,这会导致返回后仍处于异常模式,且状态标志可能错误。

因此,这一条指令完成了:

  • 恢复 r4~r12 的原始值。

  • 从堆栈中弹出返回地址给 pc,跳回原程序。

  • 同时将 SPSR 恢复到 CPSR,完成模式切换和状态恢复。

六、补充

注意:代码段area reset中的reset不要和其他的汇编文件重名了,会报错

本文没有讲解全部的数据指令,这里简单列出:

相关推荐
今儿敲了吗1 小时前
python基础学习笔记第七章——文件操作
笔记·python·学习
ADHD多动联盟2 小时前
提升自控力差孩子的学习生活:有效的学习障碍帮助与冲动控制训练方法
学习·学习方法·玩游戏
Nan_Feng_ya2 小时前
基于STM32的智能手表复刻成功(完全开源)
arm开发·stm32·pcb工艺·智能手表
次旅行的库2 小时前
【问渠哪得清如许-数据分析】学习笔记-下
数据库·笔记·sql·学习
Dfreedom.2 小时前
机器学习经典算法全景解析与演进脉络(监督学习篇)
人工智能·学习·算法·机器学习·监督学习
sheji34162 小时前
【开题答辩全过程】以 基于Android的奥运英语学习软件的设计与实现为例,包含答辩的问题和答案
学习
吃杠碰小鸡2 小时前
Python+Ai学习流程
人工智能·python·学习
夏星印3 小时前
学习吴恩达课程机器学习笔记
人工智能·笔记·学习·机器学习·ai
xuansec3 小时前
PHP 反序列化漏洞学习笔记(CTF向总结)
笔记·学习·php