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)
...
注意函数内部首先需要保存自身要用到的寄存器入栈,因此参数的获取需要根据保存的数量通过计算从栈中得到。函数退出和调用完成后别忘了释放栈空间,避免内存泄漏。