烦
2022/12/23阅读:60主题:橙心
arm底层奠定基础 (汇编)
arm底层汇编
目录
前言
在学习和工作中,遇到关于嵌入式程序运行的困惑,通过底层汇编,可以知道arm怎么做到保护现场、linux怎么做到安全运行和系统调用等。
arm发展简介

指令集的设计是处理器结构中最重要的一个部分,用ARM的术语称之为ISA(Instruction Set Architecture)。根据不同的指令集进行区分,ARM11芯片之后,也就是从ARMv7架构开始,ARM的命名方式有所改变。新的处理器家族,改以Cortex命名,并分为三个系列,分别是Cortex-A,Cortex-R,Cortex-M。(A、R、M),比较熟悉,stm32F4是cortex-M4,imx6ull是cortex-A7。2011 年 10 月,arm公司推出armv8架构,从之前32位到64位,支持64位指令集,在内存、虚拟化和安全有了一定的提升。2021年,arm已经推出armv9。
一、32位 ARMv7
以cortex-M为例
ARM主要有7种基本工作模式,USER、FIQ、IRQ、Supervisor、Abort、Undef和System。如果cortex-A会多出2种,安全监控模式(mon):可在安全模式和非安全模式下转换;HYP虚拟化模式。


1、bank寄存器
带有三角,表示bank 寄存器,该模式下独有的寄存器,没有带三角,表示各个模式共用这部分寄存器。
什么是寄存器?
存放数据的地方,cpu内部访问,读取速度最快。
2、特殊寄存器
r15 PC程序计数器(Program Counter),存储下一条要执行的指令的地址。
r14 LR 连接寄存器(Link Register ),保存函数返回地址,当通过BL或BLX指令调用函数时,硬件自动将函数返回地址保存在R14寄存器中。当函数完成时,将LR值传到PC,即可返回到被调用位置。
r13 SP 堆栈指针(Process Stack Pointer),保护现场和恢复现场要用,当发生异常的时候,硬件会把当前状态(使用到寄存器数值)保存在堆栈中,SP保存这个堆栈指针,异常处理完成,通过SP出栈,恢复到异常前的状态。
**CPSR程序状态寄存器(current program status register)**,CPSR和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义.而CPSR寄存器是按位起作用的,也就是说,它的每一位都有专门的含义。


二、汇编指令
思考下C语言编译流程,预处理(preprocessing),对头文件和宏展开→编译(compilation),将展开后的程序转成汇编代码→汇编,将汇编代码转成零一二进制机器码,目标文件;链接,将多个目标文件链接成可执行的文件。
现在我们介绍汇编程序,汇编程序相比C语言,有执行效率高的特点,但是可读性差。汇编程序编译开始流程差不多是C语言汇编步骤的开始,此编译非彼编译。汇编语言,包括两种指令,一种是汇编指令,一种是伪指令。汇编指令有对应的机器码,伪指令并没有对应的机器码,最终不会被CPU执行,而是会被编译器所执行。
1、基础指令
;将立即数3 放在r13 注意:mov指令只能用于通用的寄存器
mov r13,#3
;cpsr是特殊寄存器
mrs r0,cpsr
msr cpsr,r0
;相与
and r0,r0,#0xFFFFFFE0
;相或
orr r0,r0,#0x10
;逻辑左移
mov r0,r1,LSL#2
mov r0,r1,LSR#2
;比较指令
cmp r0,#0
;相等 r1 = 0
moveq r1,#0
;大于 r1 = r1 + 3
addgt r1,r1,#3
;跳转到标号为main地代码处 (只能短跳转32M)
b main
;跳转函数func,并保存下一条要执行的指令的位置到 lr寄存器, 当跳转代码结束后,用mov pc,lr指令跳回来
bl func
;相等(指CPSR寄存器中的Z条件码置位时)时,跳转到地址addr处
beq addr
;不等时,跳转到地址addr
bne addr
bic r0,r0,#0x0B ;清除r0中的位 0、1、和 3
tst r0,#0x20 ;测试第6位是否为0 ,为0则Z标志置1
cmp r1,r0 ;将寄存器R1的值与寄存器R0的值相减,
;并根据结果设置CPSR的标志位
2、armv7汇编demo
.text
b main
nop
nop
nop
nop
nop
nop
nop
main:
ldr r0,=buf
ldr r1,[r0]
mov r2,#5
str r2,[r0]
str r2,[r0,#8]
main_end:
b main_end
对应的机器码:

数据不合法
;立即数不合法 因为机器码中只有低12位是存储数据位 借助伪指令,编译器进行编译
mov r3,#0x1101
;使用伪指令
ldr r3,=0x1101
;但是这条指令却是合法的
mov r3,#0x1100000
;立即数合法性判断:循环移动到低8位能放下,并且移动次数是偶数次。
;使用伪指令可以避免,立即数合法判断。
机器码:


立即数合法性判断:循环移动到低8位能放下,并且移动次数是偶数次,0x1100000是0x11相左移动6次。
3、存储器之间不能直接拷贝,必须通过寄存器中转
ldr r0,[r7] ;4字节载入
ldrb r0,[r7] ;1字节载入
ldrh r0,[r7] ;2字节载入
ldr r0,[r7,#8] ;r7加上8后,载入对应地址的内容
ldr pc,_irq ;将标号中的内容载入,机器码
ldr pc,=irq ;将标号地址载入
str r0,[r3]
str r0,[r3,#4]
str r0,[r3],#4
伪指令
.section //定义内存段
.text //将定义符开始的代码编译到代码段
.data //数据段
.if .else .endif //条件编译
.end //文件结束
.byte 0x11, ’a’,0 //定义char型数组(.byte 对应1字节)
.word 0x12, 0x445566 //定义int型数组(. word 对应4字节)
.quad 0x3FA0 //分配8字节的空间(.quad 对应8字节)
.string “abcd\0” //定义字符串
.align 4 //2^4 =16 字节对齐
ldr r0, =0xE0028008 //载入大常数0xE0028008 到r0中
.equ GPG3CON, 0XE03001C0 //定义宏
.global _start //声明_start 为全局符号
堆栈操作
stmfd sp!,{r0-r12,lr}
//将寄存器r0~r12 lr中的值存入栈中 常用于中断保护现场,! 表示会自动偏移
ldmfd sp!,{r0-r12,pc}^
//将栈中值逐个弹出到寄存器r0~r12 pc中 常用于中恢复断现场,^表示会恢复spsr到cpsr
三、中断向量表
指示中断服务程序的入口位置,中断向量的顺序是固化,不可改变。(是顺序,不是地址,起始地址可以变,偏移地址)

当发生中断或者异常时,arm会自动跳到对应中断入口,由硬件完成。
以stm32启动文件为例,startup_stm32f10x.md.S
...
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY ;只读数据段
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
;DCD 连续 一段连续空间 外部中断发生时,把要处理的动作放在EXTI0_IRQHandler中断服务函数
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; External Interrupts
DCD WWDG_IRQHandler ; Window Watchdog
DCD PVD_IRQHandler ; PVD through EXTI Line detect
DCD TAMPER_IRQHandler ; Tamper
DCD RTC_IRQHandler ; RTC
DCD FLASH_IRQHandler ; Flash
DCD RCC_IRQHandler ; RCC
DCD EXTI0_IRQHandler ; EXTI Line 0
DCD EXTI1_IRQHandler ; EXTI Line 1
......
.text
;---------vector table
b main ;0x00 reset
nop ;0x04 undef
ldr pc,_sirq_hander ;0x08 soft irq
nop ;0x0c prefecth abort
nop ;0x10 data abort
nop ;0x14 reserved
nop ;0x18 irq
nop ;0x1C fiq
_sirq_hander:
.word sirq_hander
;----handler vector table
sirq_hander:
stmfd sp!,{r0-r12,lr} ; 入栈 保护现场
ldr r0,[lr,#-4] ; 获取到软件中断 号 是1还是2
bic r0,#0xff000000
cmp r0,#1
addeq r2,#1
cmp r0,#2
subeq r2,#1
sirq_hander_end:
ldmfd sp!,{r0-r12,pc} ;出栈 恢复现场
;----------app----
main:
ldr sp,=stack_buf ;保存栈地址
mov r1,#1
mov r2,#2
swi 0x1 ;软中断 让linux陷入内核 中断号1
cmp r2,#2
moveq r4,#4
movne r4,#6
swi 0x2 ;中断号2
mov r0,#3
main_end:
b main_end
.data
.space 15*4 ;数据段
stack_buf: ;因为栈是向下增长,递减,这里要注意。
.end
四、解惑
1、linux 上层最终怎么进入内核层?
通过系统调用,即swi指令软中断,进入异常,改变arm权限,获取访问硬件的权限。
2、中断怎么做到现场保护和恢复现场?
在进入中断,一般是user模式切换到异常模式(应用层到内核层),因为中断需要用到cpu的寄存器(有共用寄存器),为了不破坏原有的工作环境,会对前一个模式进行现场保护,会对各个寄存器值和代码段地址保存到栈中,记录栈指针到SP。当中断完成后,通过SP堆栈指针,出栈,恢复现场,恢复成之前的环境。
3、当man函数中,调用外部函数时,外部函数执行完,怎么返回到main函数继续执行后面代码?
LR寄存器保存函数返回地址。mov PC,LR。
4、快速中断为什么会比一般中断响应快?
1.FIQ的处理优先级比IRQ更高,甚至可以打断正在执行的IRQ;
2.FIQ模式有自己独有的寄存器,而IRQ需要和其他模式共用寄存器(共用寄存器多),在中断处理的保护/恢复现场会更快;
3.在异常向量表中,FIQ处在最末尾。在异常向量表中IRQ只能保存中断处理程序的首地址,在发生IRQ时需要一次跳转;而FIQ处在最末尾,所以可以直接将FIQ模式下的中断处理程序紧接着存放,这样在处理FIQ时就少一次跳转。
5、对于c语言 i = 1,在用汇编怎么实现的?
把1放在寄存器0中,将i地址载入到寄存器1中,最后将寄存器0数据存入到i地址。C语言一条语句可能对应多个汇编指令。这里可以联想为什么会有系统中会有锁和原子的机制,在上层一条语句其实对应底层多条指令,特别是对多线程而言,竞争访问同一资源的情况。
mov r0,#1
ldr r1,=i
str r0,[r1]
armv8 64位
armv8相对armv7增加哪些?以下是arm社区回答


在内存、虚拟化和安全有了一定的提升,执行状态可在AArch64和AArch32来回切换,兼容之前32位arm指令集。

AArch64中,已经没有User、SVC、ABT等处理器模式的概念,但ARMv8需要向前兼容,在AArch32中,就把这些处理器模式映射到了4个Exception level。
异常类型:SError 系统错误,FIQ 快速中断,IRQ一般中断和Synchronous同步异常。
异常级别有,EL0、EL1和EL2和EL3,

每个异常等级都有自己的异常向量表。异常向量表中的每一项都会保存有异常处理的跳转函数,然后跳转过去处理异常。每个向量表基虚拟地址是由矢量基址寄存器设置的,例如VBAR_EL3,VBAR_EL2和VBAR_EL1。
每个表有16个条目,每个条目的大小为128字节(32条指令)。ARMv8的向量表如下图所示,可以看到每一种异常都有固定的偏移地址。
SVC指令可以用来从EL0的用户应用程序调用到EL1的内核。HVC和SMC系统调用指令以类似的方式将处理器移动到EL2和EL3。当处理器在EL0(应用程序)执行时,它不能直接调用管理程序(EL2)或安全监视器(EL3)。
2、armv8基础汇编指令
b.ne label //不等时跳转
cbz w10, 1f //w10值等于0的适合,跳转导1f
ret //子程序返回指令,返回地址默认保存在LR(X30),代替了mov pc,lr
ldr x0,=__main //大范围的地址读取:把标号__main(地址)读入x0
adr x0,vector //小范围的地址读取:把标号vector(地址)读入x0,标号距当前指令PC的偏移小于1M
stp x29, x30, [sp, #-16]!
//入栈:把x29, x30 存储到sp-16指向的空间后,sp自减16 (因寄存器是8字节,栈是向下生长故是 -16)
//类似前索引: *(sp-16) = x29,x30 sp=sp-16 (!使得sp能自更新) 把 x29,x30看成整体
//stp只支持2个寄存器,代替了复杂的stmfd (64位汇编,取消了批量操作指令)
ldp x29, x30, [sp],#16 //出栈: 把sp指向的空间内容载入到x29, x30后,sp加16
//类似后索引: x29,x30=*sp sp=sp+16
mrs x0, sctlr_el1 //读sctlr_el1内容到x0 (注:系统寄存器,都通过mrs msr来操作)
msr sctlr_el1, x0 //写x0内容到 sctlr_el1
svc #2 //系统调用指令(触发一个同步异常,cpu则会陷入EL1)
.global _start //声明_start 为全局符号(让链接脚本能看到)
.quad 0x3FA0 //在存储器中分配8个字节,初值设为0x3FA0
.align 4 //2^4 =16 字节对齐
.macro myAdd, x,y //宏函数, 类似 myAdd(x, y)
add \x,\x,\y
.endm
myAdd x0,x2
b.ne lable //不等时跳转到标号
cbz w10, 1f //w10值等于0的适合,跳转导1f
ret //子程序返回指令,返回地址默认保存在LR(X30),代替了mov pc,lr
.macro myAdd, x,y //宏函数, 类似 myAdd(x, y)
add \x,\x,\y
.endm
myAdd x0,x2
3、armv8中断向量表示例
.globl _start
_start:
mrs x1,SPSel
mrs x2,CurrentEL
mov x0,#0
msr SPSel,x0
mov x0, #0x5
adr x0,vectors
msr vbar_el1,x0
svc #0x02 //系统调用 ,
reset_end:
b reset_end
do_bad_sync:
mov x2,#1
b reset_end
do_bad_irq:
mov x2,#2
b reset_end
.align 11 //2^11=2048 整个异常向量表 2K对齐 -> 通过对齐,实现向量表空间的预留
//16个异常 ,每个异常32条指令 16*32*4=2048
//16个异常,这里使用前8个
vectors:
//===============sp0===============
//---同步异常
.align 7 //2^7 1000 0000 =0x80 字节对齐
mov x0,#1
b do_bad_sync
//---irq异常
.align 7 //2^7 1000 0000 =0x80
mov x0,#1
b do_bad_irq
//---fiq异常
.align 7
mov x0,#1
b reset_end
//---SError异常
.align 7
mov x0,#1
b reset_end
//===============sp_elx===============
//---同步异常
.align 7 //2^7 1000 0000 =0x80 字节对齐
mov x0,#1
b do_bad_sync
//---irq异常
.align 7 //2^7 1000 0000 =0x80
mov x0,#1
b do_bad_irq
//---fiq异常
.align 7
mov x0,#1
b reset_end
//---SError异常
.align 7
mov x0,#1
b reset_end
如果有帮助,可以关注 小昭debug,有学习资料等你拿。
作者介绍