第五章 LED程序涉及的编程知识
5.1 ARM架构的简单介绍
目前IMX6UL是使用Cortex-A7架构,本小节简单介绍一下Cortex-A7架构的基础知识,比如运行模式、寄存器组等。
参考资料:
- 文件原名DEN0013D_cortex_a_series_PG.pdf
- 文档全名ARM® Cortex™-A Series Version: 4.0 Programmer’s Guide.pdf
- 文档所在目录: 资料光盘 00_UserManual参考资料Arm架构参考资料ARMv7编程手册(DEN0013D_cortex_a_series_PG).pdf
- 参考章节: 3: ARM Processor Modes and Registers
5.1.1 运行模式
Cortex-A7架构的运行模式有9种,分别为User、Sys(System)、FIQ、IRQ、ABT(Abort)、SVC(Supervisor)、UND(Undef)、MON(Monitor)、Hyp模式,如下表:
模式 | 描述 |
---|---|
User | 用户模式,非特权模式,大部分程序运行的 时候就处于此模式 |
Sys(System) | 系统模式,用于运行特权级的操作系统任务 |
FIQ | 快速中断模式,进入 FIQ 中断异常 |
IRQ | 一般中断模式 |
ABT(Abort) | 数据访问终止模式,用于虚拟存储以及存储保护 |
SVC(Supervisor) | 超级管理员模式,供操作系统使用 |
UND(Undef) | 未定义指令终止模式 |
MON(Monitor) | 用于安全扩展模式 |
Hyp | 用于虚拟化扩展 |
除了User模式属于非特权模式,其它8种处理器模式都是特权模式。
运行模式可以通过软件进行任意切换,也可以通过中断或者异常来进行切换。大多数的程序都运行在用户模式,用户模式下是不能访问系统所有资源的,有些资源是受限的,要想访问这些受限的资源就必须进行模式切换。但是用户模式是不能直接进行切换的,用户模式下需要借助异常来完成模式切换,当要切换模式的时候,应用程序可以产生异常,在异常的处理过程中完成处理器模式切换。
5.1.2 寄存器组
本节我们要讲的是 Cortex-A7 内核寄存器组,而不是芯片外设寄存器。
上一小节我们讲了 Cortex-A7 有 9 种运行模式,每一种运行模式都有一组与之对应的寄存器组,如下图:
浅色字体是与 User 模式所共有的寄存器,浅蓝色背景是各个模式所独有的寄存器,即在所有的模式中,低寄存器组(R0~R7)是共享同一组物理寄存器的,只是一些高寄存器组在不同的模式有自己独有的寄存器,比如 FIQ 模式下 R8~R14 是独立的物理寄存器。
如果某个程序处于 FIQ 模式下访问寄存器 R13(SP),那它实际访问的是寄存器 SP_fiq
如果某个程序处于 SVC 模式下访问寄存器 R13(SP),那它实际访问的是寄存器 SP_svc
9 种运行模式的寄存器合计有34个,可以分为:
- 未备份寄存器,即 R0~R7
- 备份寄存器,即 R8~R14
- 程序计数器 ,即 R15
- 程序状态寄存器
下面一一介绍以上4类寄存器。
5.1.2.1 未备份寄存器
未备份寄存器指的是 R0R7,因为在所有的运行模式下R0R7寄存器都是同一个物理寄存器,在不同的模式下,R0R7寄存器中的数据就会被破坏,所以R0R7寄存器并没有被用作特殊用途。
5.1.2.2 备份寄存器
备份寄存器中的 R8~R12 寄存器有两种物理寄存器,在快速中断模式下(FIQ)它们对应着Rx_irq(x=8~12)物理寄存器,其他模式下对应着 Rx(8~12)物理寄存器。FIQ 是快速中断模式,这个中断模式要求快速执行!因为 FIQ 模式下的 R8~R12 是独立的,因此中断处理程序可以不用执行保存和恢复中断现场的指令,从而加速中断的执行过程。
备份寄存器 R13(SP) ,也叫栈指针,有 8 个物理寄存器,其中一个是User和Sys模式共用的,剩下的 7 个分别对应 7 种不同的模式。
备份寄存器 R14(LR) ,也叫链接寄存器,有 7 个物理寄存器,其中一个是User、Sys和Hyp模式所共有的,剩下的 6 个分别对应 6 种不同的模式,主要有如下用途:
使用 R14(LR)来存放当前子程序的返回地址,如果使用 BL 或者 BLX来调用子函数的话,R14(LR)被设置成该子函数的返回地址,在子函数中,将 R14(LR)中的值赋给 R15(PC)即可完成子函数返回,如mov pc,lr
5.1.2.3 程序计数器
程序计数器 R15(PC),保存着当前执行指令地址值加 8 个字节
因为ARM处理器是三级流水线:取指->译码->执行,循环执行。比如当前正在执行第一条指令的同时也对第二条指令进行译码,第三条指令也同时被取出存放在 R15(PC)中,即 R15(PC)总是指向当前正在执行指令地址再加上 2 条指令的地址,对于 32 位的 ARM 处理器,每条指令是 4 个字节,
所以R15(PC) = 当前执行指令地址 8个字节
5.1.2.4 程序状态寄存器
程序状态寄存器PSR可以分成当前程序状态寄存器CPSR与备份程序状态寄存器SPSR。
所有运行模式都共用一个 CPSR 物理寄存器,因此 CPSR 可以在任何模式下被访问,该寄存器包含条件标志位、中断禁止位、当前运行模式标志等一些状态位以及一些控制位。但是所有运行模式都共用一个 CPSR 必然会导致冲突,因此除了 User 和 Sys 模式以外,其他 7 个模式都配备一个专用的物理状态寄存器,叫做 备份程序状态寄存器(SPSR),当特定异常中断发生时,SPSR用来保存CPSR的值,当异常退出以后可以用 SPSR 中保存的值来恢复 CPSR。
由于 SPSR 是 CPSR 的备份,因此 SPSR 和 CPSR 的寄存器结构相同,如下图:
N(bit31):当两个有符号整数运算(补码表示)时,结果用N表示,N=1/0 表示 负数/正数
Z(bit30):对于 CMP 指令,Z=1 表示进行比较的两个数大小相等
C(bit29):
在加法指令中,当结果产生了进位,则C=1,表示无符号数运算发生上溢,其它情况下 C=0
在减法指令中,当运算中发生借位,则C=0,表示无符号数运算发生下溢,其它情况下 C=1
对于包含移位操作的非加/减法运算指令,C 中包含最后一次溢出的位的数值
对于其它非加/减运算指令,C 位的值通常不受影响
V(bit28):对于加/减法运算指令,当操作数和运算结果表示为二进制的补码表示的带符号数时,V=1 表示符号位溢出,通常其他位不影响 V 位
Q(bit27):仅 ARM v5TE_J 架构支持,表示饱和状态,Q=1/0 表示累积饱和/累积不饱和
IT1:0 和 IT7:2一起组成 IT[7:0],作为 IF-THEN 指令执行状态
J(bit24)和T(bit5):控制指令执行状态,表明本指令是ARM指令还是Thumb指令,如表
J | T | 描述 |
---|---|---|
0 | 0 | ARM |
0 | 1 | Thumb |
1 | 1 | ThumbEE |
1 | 0 | Jazelle |
GE3:0:SIMD 指令有效,大于或等于
E(bit9):大小端控制位,E=1/0 表示大/小端模式
A(bit8):禁止异步中断位,A=1 表示禁止异步中断
I(bit7):I=1/0 代表 禁止/使能 IRQ
F(bit6):F=1/0 代表 禁止/使能 FIQ
M[4:0]:运行模式控制位,如表
M[4:0] | 运行模式 |
---|---|
10000 | User 模式 |
10001 | FIQ 模式 |
10010 | IRQ 模式 |
10011 | Supervisor(SVC)模式 |
10110 | Monitor(MON)模式 |
10111 | Abort(ABT)模式 |
11010 | Hyp(HYP)模式 |
11011 | Undef(UND)模式 |
11111 | System(SYS)模式 |
5.2 汇编与机器码、汇编指令
参考资料:
- 文件原名DDI0406C_d_armv7ar_arm.pdf
- 文档全名ARM® Architecture Reference Manual ARMv7-A and ARMv7-R edition
- 文档所在目录: 资料光盘 00_UserManual参考资料Arm架构参考资料 armv7 ar架构参考手册 学习CPU架构、内存及系统架构(DDI0406C_d_armv7ar_arm).pdf
- 参考章节: A5: ARM Instruction Set Encoding
根据指令复杂度来区分,所有CPU可以分为2类:
- CISC 复杂指令集计算机,Complex Instruction Set Computer,比如x86
- RISC 精简指令集计算机,Reduced Instruction Set Computing,比如ARM,RISC-V
比如,对于加法运算:a = a b,它涉及4个步骤的操作:读出a,读出b,计算a b,把结果写回a。
- 使用CISC(复杂指令集计算机,比如x86)提供的加法指令,只需要一条指令即可完成这4步操作。当然,这一个指令需要多个CPU周期才可以完成。
- 而RISC不提供“一站式”的加法指令,需调用四条单CPU周期指令完成两数相加:内存a加载到寄存器,内存b加载到寄存器,两个寄存器中数相加,寄存器结果存入内存a
ARM芯片属于精简指令集计算机(RISC:Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:
- 对内存只有读、写指令
- 对于数据的运算是在CPU内部实现
- 使用RISC指令的CPU复杂度小一点,易于设计
5.2.1 汇编与机器码
上面的例子中,数值a原来是保存在内存里的,执行了某条指令后,它的值被读入内存,那问题来了:
- 什么指令,可以让CPU从内存里把数据读进来? 比如:
mov r0, #addr_a // 把变量a的地址传给CPU寄存器r0
ldr r1, [r0] // 从r0所指的内存把数值读进CPU寄存器r1
- 读进来后,这个数保存在哪里? 当然是保存在CPU内部了,存在某个寄存器里,上面的代码用寄存器r1来保存该值
- 如何处理数据? CPU执行加法指令,比如:
add r1, r1, r2 // 在CPU内部,r1=r1 r2
- 最终数据怎么写入内存? CPU执行指令,比如:
str r1, [r0] // 将r1的值写入r0所指的内存
上面例子中,mov、add、ldr、str等都是汇编指令,或者说它们是“助记符”──帮助我们记忆的。记忆什么呢?这些指令其实是一个一个数值,我们去记这些数值有难度,所以就用mov表示某个指令的数值,用add表示某个指令的数值,对应的,这些指令的数值就是机器码,即汇编指令是机器码的助记符
ARM指令机器码是有一定格式,如下:
cond | op1 | op | 指令类型 |
---|---|---|---|
not 1111 | 00x | - | 数据处理和杂项指令,如MOV |
not 1111 | 010 | - | 加载/存储指令,如LDR/STR |
not 1111 | 011 | 0 | 加载/存储指令,如LDR/STR |
not 1111 | 011 | 1 | 媒体指令(英文:Media instructions) |
not 1111 | 10x | - | 分支指令,如B、BL; 块数据传输指令,如 LDM/STM、POP/PUSH |
not 1111 | 11x | - | 协处理器指令 |
1111 | - | - | 无条件指令,如BL |
下面讲解几种常用的汇编指令。
参考资料: ARM® and Thumb®-2 Instruction Set Quick Reference Card.pdf (ARM指令快速参考卡)
文档所在目录: 资料光盘 00_UserManual参考资料Arm架构参考资料 ARM® and Thumb®-2 Instruction Set Quick Reference Card.pdf
5.2.2 汇编指令
汇编指令的格式,如下:
代码语言:javascript复制label:
instruction @ comment
label,即标签,表示地址位置,可以通过label得到指令/数据地址
instruction,即指令,表示汇编指令或伪指令
@ comment,@表示后面是注释,comment表示注释内容
比如:
代码语言:javascript复制add:
mov r0, #0 @ 将R0寄存器设置成0
上面汇编代码中,add表示标签,mov r0, #0表示指令,@ 将R0寄存器设置成0 表示 注释
常用的汇编指令一般有mov、bl/b、add/sub、ldm/stm、push/pop等等,下面一一介绍。
5.2.2.1 mov
代码语言:javascript复制mov r1, #10 @ 将10赋值给寄存器r1,即r1=10
指令执行过程,如下:
- 取指 假设从内存的addrA地址取机器码e3a0100a(即mov r1, #10指令)
- 译码 原来是MOV指令
- 执行 CPU内部寄存器R1等于10 其中,机器码e3a0100a,MOV指令各位的解析如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-klSoVHWv-1642059925428)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image006.png)]
[31:28]位是条件码0xe;[15:12]位是寄存器R1,即0x1;[12:0]位是立即数10,即0x00a
5.2.2.2 bl
代码语言:javascript复制1 bl test_tag
2 mov r1, #10
3
4 test_tag:
5 mov r3, #0
6 mov pc, lr
第1行,跳转到test_tag标签处执行mov r3, #0指令,并且将mov r1, #10指令的地址存储到 LR 寄存器
第6行,返回到mov r1, #10指令地址,并且执行mov r1, #10指令
指令执行过程,如下:
- CPU从内存的addrA地址取机器码eb000000(即bl test_tag指令),执行后,PC跳转到test_tag标签位置,即内存的addrA 8地址,从上图可知,其实test_tag标签的地址是mov r3, #0指令的地址。同时自动将内存的addrA 4地址存储在寄存器LR中
- CPU从内存的addrA 8地址取机器码e3a03000(即mov r3, #0指令),执行,CPU内部寄存器R3等于0
- CPU从内存的addrA 12地址取机器码e1a0f00e(即mov pc, lr指令),执行,PC跳转到内存的addrA 4地址
- CPU从内存的addrA 4地址取机器码e3a0100a(即mov r1, #10指令),执行,CPU内部寄存器R1等于10 其中,机器码eb000000,BL指令各位的解析如下:
imm[23:0]是PC值与标签的偏移值除以4,但是此处的偏移值是0,为什么尼?这是因为ARM采用三级流水线的方法,即取指、译码、执行指令。所以当ARM执行addrA地址的bl test_tag指令时,但是PC已经指向addrA 8地址进行取mov r3, #0指令,所以此处的偏移值是0
5.2.2.3 b
代码语言:javascript复制1 b test_tag
2 mov r1, #10
3
4 test_tag:
5 mov r3, #0
第1行,只是跳转到test_tag标签处执行mov r3, #0指令,没有跳转回去执行mov r1, #10指令
指令B与指令BL,大同小异,此处就不一一分析了,可以参数指令BL,它们的区别:是否将B/BL指令的下一条指令的地址存储到寄存器LR,BL指令会存储,B指令不会存储。
5.2.2.4 add/sub
代码语言:javascript复制1 mov r1, #10
2 add r2, r1, #4
3 sub r2, r1, #4
第1行,将寄存器r1加上4后,赋值给寄存器r2
第2行,将寄存器r1减去4后,赋值给寄存器r2
指令执行过程,如下:
CPU从内存的addrA 4地址取机器码e2812004(即add r2, r1, #4指令),执行后,CPU内部寄存器R2等于14
CPU从内存的addrA 8地址取机器码e2412004(即sub r2, r1, #4指令),执行后,CPU内部寄存器R2等于6
其中,机器码e2812004,ADD指令各位的解析如下:
[19: 16]位是源寄存器R1,即1;[15: 12]位是目标寄存器R2,即2;[11: 0]位是立即数4,即0x004;
其中,机器码e2412004,SUB指令各位的解析如下:
[19: 16]位是源寄存器R1,即1;[15: 12]位是目标寄存器R2,即2;[11: 0]位是立即数4,即0x004;
5.2.2.5 ldr/str
代码语言:javascript复制1 mov r0, #400H @ 0x400
2 mov r1, #aH @ 0xa
3 str r1, [r0]
4 ldr r2, [r0]
第3行,将寄存器R1的值0xa存储到寄存器R0指向的地址0x400
第4行,将寄存器R0指向地址0x400的数据赋值给寄存器R2
指令执行过程,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o1GEe5XB-1642059925431)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image012.png)]
- CPU从内存的addrA地址取机器码e3a00b01(即mov r0, #400H指令),执行后,CPU内部寄存器R0等于0x400
- CPU从内存的addrA 4地址取机器码e3a0100a(即mov r1, #aH指令),执行后,CPU内部寄存器R1等于0xa
- CPU从内存的addrA 8地址取机器码e5801000(即str r1, [r0]指令),执行后,寄存器R1的0xa数据存储到寄存器R0指向的地址0x400,即内存的0x400地址的值为0xa
- CPU从内存的addrA 12地址取机器码e5902000(即ldr r2, [r0]指令),执行后,寄存器R0指向的地址0x400的数据存储到CPU内部寄存器R2,即CPU内部寄存器R2等于0xa 其中,机器码e5801000,STR指令各位的解析如下:
[19: 16]位是目标寄存器R0,即0;[15: 12]位是源寄存器R1,即1;
其中,机器码e5902000,LDR指令各位的解析如下:
[19: 16]位是源寄存器R0,即0;[15: 12]位是目标寄存器R2,即2;
代码语言:javascript复制ldr sp,=0x80200000
这个是一条伪指令,即实际中并不存在这个指令,它会被拆分成几个真正的ARM指令,实现一样的效果,将0x80200000赋值给寄存器sp,即sp=0x80200000
指令执行过程,如下:
ldr sp,=0x80200000这条伪指令,被翻译成两条指令来执行,先将0x80200000存储到内存地址addrA 4处,然后通过LDR指令把寄存器SP设置成0x80200000。
如何分析ldr sp, [pc, #-4]指令的机器码e51fd004?读者可以根据上图LDR指令机器码的格式,自行进行分析。温馨提示:imm12[11: 0]位是源寄存器Rn的偏移值。
5.2.2.6 ldm/stm
ldm,多数据加载,将某地址的值赋值给某寄存器
stm,多数据存储,将某寄存器的值存储到某地址
格式:
代码语言:javascript复制ldm{cond} Rn{!}, reglist
stm{cond} Rn{!}, reglist
参数说明:
cond:前四个条件是用于数据块操作,后四个条件是用于堆栈操作
IA : 每次传送后地址加4,其中寄存器从左到右执行,例如:STMIA R0,{R1,LR} 先存R1,再存LR
IB : 每次传送前地址加4,同上
DA : 每次传送后地址减4,其中寄存器从右到左执行,例如:STMDA R0,{R1,LR} 先存LR,再存R1
DB : 每次传送前地址减4,同上
FD : 满递减堆栈
FA : 满递增堆栈
ED : 空递减堆栈
EA : 空递增堆栈
Rn:基址寄存器,即寄存器的值是起始地址
!:表示最后的地址写回到Rn中
reglist:表示寄存器范围,用 , 隔开,如{R1,R2,R6-R9}
数据块操作:
代码语言:javascript复制1 ldr r1,=0x10000000
2
3 ldmib r1!, {r0,r4-r6}
4 stmda r1!, {r0,r4-r6}
第1行,将起始地址0x10000000赋值给r1
第3行,因为使用ib,所以每次传送前地址加4,具体操作如下:
将0X10000004地址的内容赋值给R0
将0X10000008地址的内容赋值给R4
将0X1000000C地址的内容赋值给R5
将0X10000010地址的内容赋值给R6
由于!,最后的地址写回到R1中,R1=0X10000010
第4行,因为使用da,所以每次传送后地址减4,具体操作如下:
将R6存储到0X10000010地址
将R5存储到0X1000000C地址
将R4存储到0X10000008地址
将R0存储到0X10000004地址
由于!,最后的地址写回到R1中,R1=0X10000000
如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PI07EznT-1642059925434)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image016.png)]
堆栈操作:满递减堆栈
代码语言:javascript复制1 ldr sp,=0x80200000
2
3 stmfd sp!, {r0-r2} @ 入栈
4 ldmfd sp!, {r0-r2} @ 出栈
第1行,将0x80200000赋值给sp,作为堆栈的起始地址
第3行,入栈,具体操作如下:
将R2存储到0X80200000地址
将R1存储到0X801FFFFC地址
将R0存储到0X801FFFF8地址
第4行,出栈,具体操作如下:
将0X801FFFF8地址的内容赋值给R0
将0X801FFFFC地址的内容赋值给R1
将0X80200000地址的内容赋值给R2
如下图所示:
上述第3,4行汇编代码,就是所谓的入栈,出栈。也可以用push,pop指令完成入栈,出栈,如下
代码语言:javascript复制1 ldr sp,=0x80200000
2
3 push {r0-r2} @ 入栈
4 pop {r0-r2} @ 出栈
5.3 进制
目前计算机对数据的表示方式,有十六进制、十进制、八进制与二进制。
5.3.1 如何理解它们的区别?
十六进制,逢十六进一,每一位由0~F组成,习惯用0x前缀表示或用H后缀表示
代码语言:javascript复制0xA或AH
十进制,逢十进一,每一位由0~9组成,无前缀或用D后缀表示
代码语言:javascript复制10或10D
八进制,逢八进一,每一位由0~7组成,习惯用0前缀表示或用O后缀表示
代码语言:javascript复制012或12O
二进制,逢二进一,每一位由0~1组成,习惯用0b前缀表示或用B后缀表示
代码语言:javascript复制0b1010或1010B
5.3.2 在C语言中怎么表示这些进制呢?
代码语言:javascript复制十六进制:int a = 0xA; // 0x前缀
十进制: int a = 10;
八进制: int a = 012; // 0前缀
二进制: int a = 0b1010;// 0b前缀
5.3.3 十六进制与二进制转换关系
在嵌入式开发中经常需要对十六进制与二进制进行转换
如何快速的转换2/16进制? 首先记住8 4 2 1 ——>二进制权重
将二进制0b01101110101转换成十六进制:将二进制从右到左,每四个分成一组:
结果就是0x375
将十六进制0xABC1转换成二进制:将十六进制从右到左,每个分成四位:
结果就是1010 1011 1100 0001
5.4 大/小端模式与位操作
5.4.1 大/小端模式
大端模式(Big-endian),是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中
小端模式(Little-endian),是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中
比如:0x12345678,在大/小端模式的存储位置如下:
内存地址 | 大端模式 | 小端模式 |
---|---|---|
addr 3 | 0x78 | 0x12 |
addr 2 | 0x56 | 0x34 |
addr 1 | 0x34 | 0x56 |
addr | 0x12 | 0x78 |
5.4.2 位操作
5.4.2.1 移位
代码语言:javascript复制1 int a = 0x6; // 二进制是0b0110
2 int b = a<<1;
3 int c = a>>1;
第2行,对a左移一位,从0b0110->0b1100,即b=0xC
第3行,对a右移一位,从0b0110->0b0011,即b=0x3
5.4.2.2 取反
代码语言:javascript复制1 int a = 0x6; // 二进制是0b0110
2 int b = ~a;
第2行,对a按位取反,从0b0110->0b1001,即b=0x9
5.4.2.3 位与
只有对应的两个二进位都为1时,结果位才为1
代码语言:javascript复制1 int a = 0x6; // 二进制是0b0110
2 int b = 0x7; // 二进制是0b0111
3
4 int c = a&b;
第4行,a&b,二进制是0b0110,即c=0x6
5.4.2.4 位或
只要对应的二个二进位有一个为1时,结果位就为1
代码语言:javascript复制1 int a = 0x6; // 二进制是0b0110
2 int b = 0x7; // 二进制是0b0111
3
4 int c = a|b;
第4行,a|b,二进制是0b0111,即c=0x7
5.4.2.5 置位
代码语言:javascript复制1 int a = 0x6; // 二进制是0b0110
2
3 int a |= (1<<3);
第3行,将变量a的bit3置1。1<<3 = 0b1000,然后0b1000|0b0110=0b1110,即a=0xe
5.4.2.6 清位
代码语言:javascript复制1 int a = 0x6; // 二进制是0b0110
2
3 int a &= ~(1<<2);
第3行,将变量a的bit2清位。~(1<<2) = 0b1011,然后0b1011&0b0110=0b0010,即a=0x2
5.5 汇编程序调用C程序
在C程序和ARM汇编程序之间相互调用时必须遵守ATPCS规则,ATPCS规定了一些函数间调用的基本规则。
参考资料:
- 文件原名ATPCS.pdf
- 文档全名The ARM-THUMB Procedure Call Standard
- 文档所在目录: 资料光盘 00_UserManual参考资料Arm架构参考资料 ATPCS(ATM-Thumb指令调用标准).pdf
- 参考章节: 所有
5.5.1 ATPCS规则
ATPCS即ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)的简称,是基于ARM指令集和THUMB指令集过程调用的规范,规定了调用函数如何传递参数,被调用函数如何获取参数,以何种方式传递函数返回值。
寄存器R0~R15在ATPCS规则的使用:
- 在函数中,通过寄存器R0R3来传递参数,被调用的函数在返回前无需恢复寄存器R0R3的内容
- 在函数中,通过寄存器R4~R11来保存局部变量
- 寄存器R12用作函数间scratch寄存器
- 寄存器R13用作栈指针,记作SP,在函数中寄存器R13不能用做其他用途,寄存器SP在进入函数时的值和退出函数时的值必须相等
- 寄存器R14用作链接寄存器,记作LR,它用于保存函数的返回地址,如果在函数中保存了返回地址,则R14可用作其它的用途
- 寄存器R15是程序计数器,记作PC,它不能用作其他用途
5.5.2 汇编程序如何向C程序的函数传递参数
- 当参数小于等下4个时,使用寄存器R0~R3来进行参数传递
- 当参数大于4个时,前四个参数按照上面方法传递,剩余参数传送到栈中,入栈的顺序与参数顺序相反,即最后一个参数先入栈
5.5.3 C程序如何返回结果给汇编程序
- 结果为一个32位的整数时,通过寄存器R0返回
- 结果为一个64位整数时,通过R0和R1返回,依此类推.
- 结果为一个浮点数时,通过浮点运算部件的寄存器f0,d0或s0返回
- 结果为一个复合的浮点数时,通过寄存器f0-fN或者d0~dN返回
- 对于位数更多的结果,通过调用内存来传递
5.5.4 C函数为何要用栈
总的来说,栈的作用就是:保存现场/上下文,传递参数
- 保存现场/上下文
保存现场,也叫保存上下文
现场,相当于案发现场,总有一些现场的情况,要记录下来的,否则被别人破坏掉之后,你就无法恢 复现场了。而此处说的现场,就是指CPU运行的时候,用到了一些寄存器,比如R0~R3,LR等等,对于这些寄存器的值,如果你不保存而直接跳转到函数中去执行,那么很可能会被破坏了,因为函数执行需要用到这些寄存器。
因此在函数调用之前,应该将这些寄存器等现场,暂时保持起来,等调用函数执行完毕返回后,再恢复现场,这样CPU就可以正确的继续执行了。
保存寄存器的值,一般用的是push指令,将对应的某些寄存器的值,一个个放到栈中,即所谓的入栈。
然后待被调用的子函数执行完毕的时候,再调用pop,把栈中的一个个的值,赋值给对应的入栈的寄存器,即所谓的出栈。
- 传递参数
当函数被调用并且参数大于4个时,(不包括第4个参数)第4个参数后面的参数就保存在栈中。
5.6 C语言中读写寄存器
每一个寄存器都有一个地址,只要找到寄存器地址,通过指针指向寄存器地址单元,通过读写指针值,就可以获得寄存器值。
首先,定义一个指针,指针类型根据寄存器大小决定,同时需要加上volatile关键字让编译器不要优化此指针,比如,CCM_CCGR1寄存器值是32位,此处定义为unsigned int *指针类型,寄存器地址为0x20C406C
代码语言:javascript复制volatile unsigned int *CCM_CCGR1 = (volatile unsigned int *)(0x20C406C);
然后,对寄存器进行读写操作
代码语言:javascript复制val = *CCM_CCGR1; // 读寄存器
*CCM_CCGR1 |= (3<<30); // 写寄存器,将CCM_CCGR1寄存器的[31:30]位置1
5.7 start.S解析
代码语言:javascript复制2 .text
3 .global _start
4 _start:
- 第2行,.text表示代码段,汇编系统预定义段名,说明下面的汇编是代码段
- 第3行,.global表示_start是一个全局符号
- 第4行,标签_ start,汇编程序的默认入口是 _ start,也可以在链接脚本中使用ENTRY来指明其它的入口点,类似C语言main()函数,_ start是整个程序的入口,即程序执行的第一条指令
@ 相当于一个函数,_start是函数名,下面汇编指令是函数内容
4 _start:
5
6 //设置栈
7 ldr sp,=0x80200000
8
9 bl clean_bss
10
11 bl main
12
13 halt:
14 b halt
- 第7行,将0x80200000赋值给寄存器sp,即设置栈地址,因为C语言函数调用时,保存现场/上下文和传递参数需要用到栈
- 第9行,跳转到标签clean_bss,相当于调用clean_bss函数,并将bl main指令地址存储到寄存器lr中
- 第11行,进入C语言的main()函数,并将b halt指令地址存储到寄存器lr中
- 第13行,标签halt
- 第14行,只跳转到标签halt,循环执行b halt指令执行
@ 相当于一个函数,clean_bss是函数名,下面汇编指令是函数内容
16 clean_bss:
17 /* 清除BSS段 */
18 ldr r1, =__bss_start
19 ldr r2, =__bss_end
20 mov r3, #0
21 clean: @ 下面汇编指令相当于循环体,直到R1与R2相等
22 str r3, [r1]
23 add r1, r1, #4
24 cmp r1, r2
25 bne clean
26
27 mov pc, lr @ 函数执行完毕,返回
- 第16行,标签clean_bss,下面汇编代码是清除BSS段,将BSS段都设置成0的作用
- 第18行,将链接脚本定义的bss起始地址赋值给寄存器r1
- 第19行,将链接脚本定义的bss结束地址赋值给寄存器r2
- 第20行,将0赋值给寄存器r3,即r3=0
- 第21行,标签clean
- 第22行,将寄存器r3的值存储到寄存器r1的值对应地址中
- 第23行,将寄存器r1的值加上4,赋值给寄存器r1,即r1 = r1 4
- 第24行,比较寄存器r1的值与寄存器r2的值
- 第25行,如果寄存器r1的值与寄存器r2的值不相等,跳转到标签clean
- 第26行,如果寄存器r1的值与寄存器r2的值相等,就执行此行,返回到 bl main 处,继续执行
5.8 根据led.dis分析代码的整体运行流程
在分析led.dis文件前,我们再把imx6ull芯片如何将led.bin文件复制到内存DDR中过程,简单整体过一篇。
如下图,imx6ull芯片一上电后,会先执行bootRom程序,此程序是芯片出厂时已经固定的程序,除了芯片原厂,咱们是无法修改的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uvCdRLif-1642059925437)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image020.png)]
bootRom有什么作用?下面一一讲解。
- bootRom会把EMMC或TF卡的前4K数据读入到芯片内部RAM运行
- bootRom根据DCD进行初始化DDR。
- bootRom根据IVT,从EMMC或TF卡中将led.bin读到DDR的0x80100000地址
- 跳转到DDR的0x80100000地址执行
目前led.bin程序已经复制到内存中,CPU开始从内存0x80100000地址开始执行机器码,每一条机器码是32位/4字节,此处的机器码就是led.bin中的机器码,那我们能不能打开led.bin文件,看到里面的机器码?答案是可以的。如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dGwtyJsS-1642059925437)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image021.png)]
前面介绍过大/小端模式,你是否记得?如果忘记了,可以回头看一下。 此处可以看到机器码e59fd028(指令:ldr sp,=0x80200000)的存储形式:
地址 | 机器码 |
---|---|
00000000 | 28 |
00000001 | d0 |
00000002 | 9f |
00000003 | e5 |
没错,imx6ull的存储方式是小端模式,换一句话说,ARM存储方式一般都是小端模式。 但是bin文件的机器码不方便阅读,所以我们一般会通过objdump进行反汇编,得到人类容易读的led.dis文件。 如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jcXyHisv-1642059925438)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image022.png)]
下面我们就来分析一下led.dis文件,但是在阅读此小节前,尽量把前一小节《1.7 start.S解析》完全理解懂,不然阅读此小节,有点云里雾里。
代码语言:javascript复制 1) CPU执行的第一条机器码就是内存地址0x80100000存储的e59fd028机器码,对应的指令是ldr sp, [pc, #40],相当于Start.S文件的ldr sp,=0x80200000指令,寄存器SP的值等于0x80200000
代码语言:javascript复制80100000: e59fd028 ldr sp, [pc, #40] ; 80100030 <clean 0x14>
- 每执行完一条机器码,会自动执行下一条内存地址0x80100004存储的eb000001机器码,对应的指令是bl 80100010,相当于Start.S文件的bl clean_bss指令。
80100004: eb000001 bl 80100010 <clean_bss>
....
80100010 <clean_bss>:
80100010: e59f101c ldr r1, [pc, #28] ; 80100034 <clean 0x18>
80100014: e59f201c ldr r2, [pc, #28] ; 80100038 <clean 0x1c>
- 跳转到内存地址0x80100010,执行e59f101c机器码,对应的指令是ldr r1, [pc, #28],相当于Start.S文件的ldr r1, =__bss_start指令。
- 此处clean_bss相当于一个函数体,CPU会自动让内存地址加4,向下执行机器码,直到执行mov pc, lr指令后,才返回内存地址0x80100008处执行fa000057机器码,对应的指令是blx 8010016c,相当于Start.S文件的bl main指令。 到此,CPU跳转到C语言的main()函数,继续执行。 为了让大家深入理解C语言函数的调用执行过程中,汇编指令如何执行,此处简单分析main()函数
如上图所示
1.进入main()函数后,先将寄存器R7、LR入栈,保存现场/上下文,方便main()函数执行完毕后返回,并且将当前栈指向的内存地址赋值给寄存器R7,如图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VkW5Adpe-1642059925438)(https://cdn.jsdelivr.net/gh/DongshanPI/HomeSite-Photos@main/IMX6ULL-BareMetal/LED_Program_Knowdge_image024.png)]
2. 调用led_init()函数,因为没有参数传递,所以直接调用BL指令进行跳转,即bl 8010003c指令
3. 调用led_ctl(1)函数,此处只有一个参数,通过寄存器R0进行传递,即movs r0, #1指令,然后通过BL指令进行跳转,即bl 801000f8指令,关于参数传递问题,可以参考前面《5.5 汇编程序调用C程序》。
4. 调用delay(1000000)函数,此处只有一个参数,通过寄存器R0进行传递,然后通过BL指令进行跳转
5. 调用led_ctl(0)函数,此处只有一个参数,通过寄存器R0进行传递,然后通过BL指令进行跳转
6. 调用delay(1000000)函数,此处只有一个参数,通过寄存器R0进行传递,然后通过BL指令进行跳转
7. while(1)循环体到此已经结束,但是需要循环执行循环体的内容,通过B指令进行跳转到循环体开头,即b.n 80100174指令,执行内存地址0x80100174处的指令,也就是led_ctl(1)函数对应的汇编指令movs r0, #1
到此,进入并执行main()函数对应的汇编指令分析已经结束,如果读者有兴趣可以分析一下,led_init()、led_ctl()与delay()函数的汇编指令。