最近学习了ARM汇编,汇编和C语言思维完全不一样。C语言我们写while、for循环,写函数调用,编译器默默帮我们处理跳转、保存寄存器、栈操作。到了汇编这一层,所有这些工作都要我们亲手写出来,每一步CPU干了什么,都得心里有数。
一、汇编里实现循环
不管是C语言的do‑while、while还是for循环,底层翻译成汇编,本质都离不开循环三要素:循环结束条件、让循环走向结束的语句、循环体。
do‑while:先执行循环体,再判断条件。对应汇编就是先做运算,再比较,条件满足就跳回继续循环。
举个例子求0~100累加:先做相加、变量自增,再用cmp做比较,ble小于等于就跳回标签继续跑。
mov r0, #0 ;i
mov r1, #0 ;sum
loop
add r1, r1, r0
add r0, r0, #1
cmp r0, #100
ble loop
while / for:先判断条件,满足才执行循环体。汇编里就要把cmp比较放在最前面,如果条件不满足直接跳出循环。
同样是0‑100求和,上来先判断,如果i大于100直接跳到结束标签,否则执行循环内容。
mov r0, #0
mov r1, #0
loop
cmp r0, #100
bgt finish
add r1, r1, r0
add r0, r0, #1
b loop
finish
b finish
总的来说:do‑while是先干活,后判断;while/for是先判断,后干活。C语言只是语法外壳,底层都是标签+比较+跳转。
二、ARM汇编函数调用
C语言写函数很轻松,写好函数,直接调用就完事。汇编里面调用函数,要面对两个很现实的问题:
-
被调用的函数会修改通用寄存器,把主函数正在用的数据冲掉;
-
函数嵌套调用的时候,返回地址容易乱掉,不知道调用完回到哪里。
解决办法就是栈,保护现场和恢复现场。
- ARM栈类型
栈分四种:满增、满减、空增、空减。
满栈:sp指针指向最后一个入栈的有效数据
空栈:sp指向下一个可以存放数据的空位
增栈:地址变大;
减栈:地址变小
ARM默认使用满减栈(FD),也就是STMFD、LDMFD这一组指令。
注意栈指针初始化不能直接写mov sp,#0x40001000,这个数不属于合法立即数,会直接报错,要用ldr sp,=0x40001000。同时Keil魔术棒里面要配置IRAM内存地址和大小,不然栈空间都没有。
stmfd sp!,{寄存器列表}:保护现场,把寄存器压入栈,!代表执行完指令自动更新sp指针
ldmfd sp!,{寄存器列表}:恢复现场,把栈里的数据弹回寄存器。特别注意,如果写ldmfd ...,{...,pc},弹栈的同时直接完成函数返回,就不用再写bx lr。
函数调用流程简单梳理:
-
设置栈指针sp;
-
主调函数先stmfd压栈,保护现场;
-
bl 函数名跳转,bl会自动把返回地址存到lr寄存器;
-
执行被调用函数;
-
返回时ldmfd弹栈恢复寄存器,回到主函数继续往下跑。
这里要记住:pc存当前执行地址,lr存放函数调用的返回地址,bx lr就是跳回返回地址。
- b、bl、bx简单区分
b 标签:单纯跳转,不会保存返回地址,跳过去就回不来;
bl 标签:跳转+把下一条指令地址保存到lr,用于函数调用,执行完可以回来;
bx 寄存器:跳转到寄存器保存的地址,还会切换ARM/Thumb指令集,函数返回常用bx lr。
三、汇编和C语言相互调用
实际开发很少全部手写汇编,大多是C为主,少量汇编。分为两种场景:汇编调用C函数、C调用汇编函数。
汇编调用C函数
-
在汇编文件里import c_add声明外部C函数;C文件用extern声明。
-
调用前做好保护现场,寄存器传参。
ARM规则:前4个参数放在r0‑r3,多于4个的参数,直接压到栈上。返回值放在r0。
示例:
stmfd sp!, {r0-r12, lr}
mov r0, #1
mov r1, #2
mov r2, #3
mov r3, #4
mov r4, #5
stmfd sp!, {r4} ;第5个参数压栈
bl c_add
ldmfd sp!, {r4}
ldmfd sp!, {r0-r12, lr}
C语言调用汇编函数
-
汇编里面用export func1把函数导出,对外可见;
-
在C文件写extern int func1(int a,int b);声明,之后C就可以直接像普通函数一样调用。
混编统一规则:参数前4个r0~r3,多余参数压栈;函数返回值放到r0寄存器,不管汇编调C还是C调汇编都遵守这个约定。
四、CPSR程序状态寄存器,修改CPU工作模式
CPSR寄存器里面存着条件标志位还有CPU工作模式M位域。
两个关键指令:
MRS Rd, cpsr:把CPSR内容读到通用寄存器,读状态寄存器;
MSR cpsr, Rn:把通用寄存器写回CPSR,修改状态寄存器。
修改模式套路:先读CPSR到普通寄存器,把M域清零,或上新的模式码,再写回CPSR。
mrs r0, cpsr
bic r0, r0, #(0x1FF << 0)
orr r0, r0, #(0x10 << 0)
msr cpsr, r0
同时CPSR的NZCV条件标志位,cmp比较指令会改变它们,之后bgt、ble这类带后缀的条件跳转就依靠这些标志位判断。
还有软中断异常swi #7,会触发异常,跳转到异常向量表,也是后面学习中断的基础。
学ARM汇编最大感受就是,C语言把很多底层细节全部封装起来了。以前写循环写函数,只管业务逻辑,现在汇编每一次跳转,每一次压栈弹栈都要自己操心。
刚开始写很容易栈没配置好、忘记保护lr、立即数报错,各种各样的编译坑。多动手敲代码,对着仿真看寄存器、sp指针的变化,一点点就能理解CPU真正在干什么。