ARM汇编基础:栈与函数
栈后进先出的特性使得它非常适用于函数内局部变量,以及其他具有短暂生命周期的数据的存取。本文讲述ARM汇编中,栈在函数中的运用。
一个函数或方法的调用一般遵循如下步骤:
- 记录函数参数
- 记录调用方地址,以便函数执行结束后跳转继续运行原程序
- 跳转至函数体
- 使用函数参数
- 执行函数体(可能包含一系列计算和对其他函数的调用)
- 记录返回值
- 跳转至调用方地址
- 获取函数返回值
如果使用固定的内存空间存储以上涉及的数据,将难以管理内存的释放从而造成内存空间浪费。尤其是当需要进行递归操作时,固定的内存空间难以被有效配置。而栈后进先出的特性刚好符合函数的调用和返回的需求。
ARM中,SP寄存器用于指向栈顶的内存地址,且栈将从一个较大的内存地址开始递减存储数据。栈有两种操作方式,PUSH
和POP
。PUSH即入栈,SP指针递减;POP即出栈,SP指针递增。
我们将使用栈进行存储:
- 函数参数
- LR寄存器的值(link register,存储外部调用方的地址用于函数返回后程序跳转继续执行)
- 计算过程中可能涉及的原寄存器中的值
- 函数返回结果
- 局部变量
- 任何其他临时的具有短暂生命周期的数据
当函数退出时,这些临时数据将被pop出栈,从而不再占用内存空间。
在使用栈存储数据时的一个重要原则是“由被调用方保存任何用到的,且可能对调用方有意义的寄存器和数据”。这些数据包括函数内部计算所需的寄存器和LR寄存器等。(若函数内不再调用其他函数则无需保存LR寄存器)
下图显示了一个栈内数据的框架,其中包含两个函数的层级调用。
一个例子如下:
me ... ; a method that finds out things about me PUSH {LR} ; push LR - me will call other methods PUSH {R0-R6} ; push any registers used by ‘me’ SUB SP, SP, #36 ; create 9 variables for ‘me’ ... ; PUSH {R0,R1,R2} ; push the three parameters for age BL age ; method call next ... ADD SP, SP, #36 ; discard the variable space POP {R0-R6} ; recover the protected registers LDR PC, [SP],#20 ; discard 4 parameters, return age ; a method that finds a person’s age PUSH {LR} ; push LR - age will call other methods PUSH {R0-R4} ; age uses R0-R4, protect for caller SUB SP, SP, #48 ; create 12 variables for ‘age’ ... ; calculating age BL SomeOtherMethod ; calculating age ... ; calculating age ADD SP, SP, #48 ; discard the variable space POP {R0-R4} ; recover the protected registers LDR PC, [SP],#16 ; discard 3 parameters, return
该例中me
函数内部调用age
函数,age
函数中也存在其他函数的进一步调用。
BL
指令将它的下一条指令的地址存储至LR寄存器中,然后跳转到函数标签指向的指令处。当函数执行结束后,可使用MOV PC, LR
跳转回调用方。PUSH
和POP
是两条伪指令,其作用是按照寄存器数值递减的顺序入栈或递增的顺序出栈,实际指令为STMFD
和LDMFD
,这里不做额外解释。当需要分配或丢弃栈空间时,可直接对SP指针执行加减操作。
另外,对于全局变量或静态变量而言,可使用DEFW
为变量分配固定内存,而不采用栈存取这类数据。
栈与函数的学习让我联想到C语言中的数据存储方式。在函数中(包括main
函数)直接声明的变量将被存储到栈中,栈有大小限制,不宜存储包含大数据量的数组等。函数中的局部变量等存储在栈中的数据随着函数的退出而销毁,无需手动管理,不会造成存储空间的浪费。而动态分配的内存(如malloc
)将存储至堆中,这些数据在使用结束后需要手动释放,否则将造成内存泄漏。
以下是一些关于栈与函数的练习题。
lab练习一:编写ARM函数实现字符串打印输出、字符串拼接、字符串复制和字符串比较操作。
字符串打印(输出首地址为R1的字符串):
B main s1 DEFB "one\0" ; 定义字符串s1 ALIGN s2 DEFB "two\0" ; 定义字符串s2 ALIGN printstring ; 定义函数标签 l1 LDRB R0, [R1], #1 ; 从R1读取一个字符到R0,同时令R1指向下一个字符 CMP R0, #0 ; 判断是否到达尾部'\0'字符 BEQ end ; 若遍历结束,则跳出循环 SVC 0 ; 字符输出(系统调用) B l1 ; 循环l1 end MOV R0, #10 ; 将回车符'\n'加载到R0 SVC 0 ; 输出回车符 MOV PC, LR ; 恢复LR中存储的调用方程序地址 main ADR R1, s1 ; 加载s1首地址到R1 BL printstring ; 将下一条指令地址存储到LR后跳转执行函数 ADR R1, s2 BL printstring
字符串拼接(将首地址为R2的字符串拼接到R1字符串后,需确保R1内存空间足够):
strcat l3 LDRB R0, [R1], #1 CMP R0, #0 BNE l3 ; 遍历直到R1字符串尾部 SUB R1, R1, #1 ; 令R1指向尾部'\0'字符 l4 LDRB R0, [R2], #1 ; 读取R2中的下一个字符 STRB R0, [R1], #1 ; 将字符拼接到R1尾部 CMP R0, #0 ; 判断是否遍历结束R2 BNE l4 MOV PC, LR
字符串复制(将R2字符串复制到R1):
strcpy l2 LDRB R0, [R2], #1 ; 读取R2中的下一个字符 STRB R0, [R1], #1 ; 存储到R1尾部 CMP R0, #0 ; 判断是否遍历到达R2尾部 BNE l2 MOV PC, LR
字符串比较,比较两个字符串R2和R3,同时进行遍历直到遇到第一个不相等字符(包含尾部’\0’字符)并比较该字符的ascii编码数值大小(若同时到达字符串尾则相等),结果反映在标志位寄存器中。该例中需包含子函数的声明和嵌套调用,子函数用于最终字符的比较。
sorted STR LR, return2 ; 保存LR,为子函数的嵌套调用做准备 l5 LDRB R4, [R2], #1 ; 加载R2的下一个字符到R4中 LDRB R5, [R3], #1 ; 加载R3的下一个字符到R5中 CMP R4, #0 ; 判断是否到达R2尾部 BEQ end2 CMP R5, #0 ; 判断是否到达R3尾部 BEQ end2 CMP R4, R5 ; 字符比较 BEQ l5 ; 若相等则继续循环,否则跳出 end2 BL function ; 子函数调用,比较最终的两个字符 LDR PC, return2 ; 从临时内存中恢复外部调用方地址 return2 DEFW 0 ; 定义临时内存,用于保存LR寄存器 function CMP R4, R5 ; 字符比较,得出结果反映在标志位寄存器中 MOV PC, LR ; 根据LR的值,回到函数体内部
lab练习二:仿照以下Python代码,完成Arm汇编代码,合理选择函数参数传递方式,注意合理利用“短路逻辑”实现较为复杂的条件判断,提高程序执行效率。
Python代码:
pDay = 23 #or whatever is today's date pMonth = 11 #or whatever is this month pYear = 2005 #or whatever is this year def printAgeHistory (bDay, bMonth, bYear): year = bYear + 1 age = 1 print("This person was born on " + str(bDay) + "/" + str(bMonth) + "/" + str(bYear)) while year < pYear or \ (year == pYear and bMonth < pMonth) or \ (year == pYear and bMonth == pMonth and bDay < pDay): print("This person was " + str(age) + " on " + str(bDay) + "/" + str(bMonth) + "/" + str(year)) year = year + 1 age = age + 1 if (bMonth == pMonth and bDay == pDay): print("This person is " + str(age) + " today!") else: print("This person will be " + str(age) + " on " + str(bDay) + "/" + str(bMonth) + "/" + str(year)) def main(): printAgeHistory(pDay, pMonth, 2000) print("Another person") printAgeHistory(13, 11, 2000) if __name__ == '__main__': main()
相同功能汇编代码:
print_char equ 0 ; Define names to aid readability stop equ 2 print_str equ 3 print_no equ 4 cLF equ 10 ; Line-feed character ADR SP, _stack ; set SP pointing to the end of our stack B main DEFS 100 ; this chunk of memory is for the stack _stack ; This label is 'just after' the stack space wasborn DEFB "This person was born on ",0 was DEFB "This person was ",0 on DEFB " on ",0 is DEFB "This person is ",0 today DEFB " today!",0 willbe DEFB "This person will be ",0 ALIGN pDay DEFW 23 ; pDay = 23 //or whatever is today's date pMonth DEFW 11 ; pMonth = 11 //or whatever is this month pYear DEFW 2005 ; pYear = 2005 //or whatever is this year ; def printDate (day, month, year) ; parameters ; R7 = day ; R8 = month ; R9 = year ; local variables (callee-saved registers) ; R0 (allow SVC to output via R0) printDate STR R0, [SP, #-4]! ; callee saves R0 MOV R0, R7 SVC print_no MOV R0, #'/' SVC print_char MOV R0, R8 SVC print_no MOV R0, #'/' SVC print_char MOV R0, R9 SVC print_no MOV R0, #cLF SVC print_char LDR R0, [SP], #4 ; callee saved register MOV PC, LR ; def printAgeHistory (bDay, bMonth, bYear) ; parameters ; R0 = bDay (on entry, moved to R6 to allow SVC to output via R0) ; R1 = bMonth ; R2 = bYear ; local variables (callee-saved registers) ; R4 = year ; R5 = age ; R6 = bDay - originally R0 printAgeHistory STMFD SP!, {R4-R9, LR} ; callee saves seven registers, R7-R9 are used for printDate MOV R6, R0 ; Get parameter from R0 (R1 and R2 still work) ; year = bYear + 1 ADD R4, R2, #1 ; age = 1; MOV R5, #1 ; print("This person was born on " + str(bDay) + "/" + str(bMonth) + "/" + str(bYear)) ADRL R0, wasborn SVC print_str MOV R7, R6 MOV R8, R1 MOV R9, R2 BL printDate ; while year < pYear or ; (year == pYear and bMonth < pMonth) or ; (year == pYear and bMonth == pMonth and bDay < pDay): loop1 LDR R0, pYear CMP R4, R0 BLO history ; if year < pYear: jump to history BNE end1 ; if year != pYear: jump to end1 LDR R0, pMonth CMP R1, R0 BLO history ; if bMonth < pMonth: jump to history BNE end1 ; if bMonth != pMonth: jump to end1 LDR R0, pDay CMP R6, R0 BHS end1 ; if bDay >= pDay: jump to end1; else: continue to execute (to history) ; print("This person was " + str(age) + " on " + str(bDay) + "/" + str(bMonth) + "/" + str(year)) history ADRL R0, was SVC print_str MOV R0, R5 SVC print_no ADRL R0, on SVC print_str MOV R7, R6 MOV R8, R1 MOV R9, R4 BL printDate ; year = year + 1 ADD R4, R4, #1 ; age = age + 1 ADD R5, R5, #1 ; //} B loop1 end1 ; if (bMonth == pMonth and bDay == pDay): LDR R0, pMonth CMP R1, R0 BNE else1 ; if bMonth != pMonth: jump to else1 LDR R0, pDay CMP R6, R0 BNE else1 ; if bDay != pDay: jump to else1; else: continue to execute ; print("This person is " + str(age) + " today!") ADRL R0, is SVC print_str MOV R0, R5 SVC print_no ADRL R0, today SVC print_str MOV R0, #cLF SVC print_char ; else B end2 else1 ; print("This person will be " + str(age) + " on " + str(bDay) + "/" + str(bMonth) + "/" + str(year)) ADRL R0, willbe SVC print_str MOV R0, R5 SVC print_no ADRL R0, on SVC print_str MOV R7, R6 MOV R8, R1 MOV R9, R4 BL printDate ; }// end of printAgeHistory end2 LDMFD SP!, {R4-R9} ; callee saved registers LDR PC, [SP], #4 another DEFB "Another person",10,0 ALIGN ; def main(): main LDR R4, =&12345678 ; Test value - not part of Java compilation MOV R5, R4 ; See later if these registers corrupted MOV R6, R4 ; printAgeHistory(pDay, pMonth, 2000) LDR R0, pDay ; Use registers for method parameters LDR R1, pMonth MOV R2, #2000 BL printAgeHistory ; print("Another person"); ADRL R0, another SVC print_str ; printAgeHistory(13, 11, 2000) MOV R0, #13 ; Use registers for method parameters MOV R1, #11 MOV R2, #2000 BL printAgeHistory ; Now check to see if register values intact (Not pat of Python code) LDR R0, =&12345678 ; Test value CMP R4, R0 ; Did you preserve these registers? CMPEQ R5, R0 ; CMPEQ R6, R0 ; ADRLNE R0, whoops1 ; Oh dear! SVCNE print_str ; ADRL R0, _stack ; Have you balanced pushes & pops? CMP SP, R0 ; ADRLNE R0, whoops2 ; Oh no!! SVCNE print_str ; End of test code ; }// end of main SVC stop whoops1 DEFB "\n** BUT YOU CORRUPTED REGISTERS! **\n", 0 whoops2 DEFB "\n** BUT YOUR STACK DIDN'T BALANCE! **\n", 0
由于程序中需要多次打印出生日期,因此单独声明一个子函数printDate
是一个很好的选择。
本例中使用寄存器来为函数提供参数传递,也可以使用栈来传参,例如:
... printAgeHistory STMFD SP!, {R0-R2, R4-R6} ; callee saves six registers (PUSH {R0-R2, R4-R6}) LDR R6, [SP, #(6 + 2) * 4] ; Get parameters from stack LDR R1, [SP, #(6 + 1) * 4] LDR R2, [SP, #(6 + 0) * 4] ... ; end of printAgeHistory end2 LDMFD SP!, {R0-R2, R4-R6} ; callee saved registers (POP {R0-R2, R4-R6}) MOV PC, LR ... ; printAgeHistory(pDay, pMonth, 2000) LDR R2, pDay LDR R1, pMonth MOV R0, #2000 STMFD SP!, {R0-R2} ; Stack three parameters (PUSH {R0-R2}) BL printAgeHistory ADD SP, SP, #12 ; Deallocate three 32-bit variables (no need to POP {R0-R2}, ADD is more efficient) ...
注意函数内部首先需要保存自身要用到的寄存器入栈,因此参数的获取需要根据保存的数量通过计算从栈中得到。函数退出和调用完成后别忘了释放栈空间,避免内存泄漏。