底层原理,  技术人生

ARM汇编基础:栈与函数

栈后进先出的特性使得它非常适用于函数内局部变量,以及其他具有短暂生命周期的数据的存取。本文讲述ARM汇编中,栈在函数中的运用。

一个函数或方法的调用一般遵循如下步骤:

  1. 记录函数参数
  2. 记录调用方地址,以便函数执行结束后跳转继续运行原程序
  3. 跳转至函数体
  4. 使用函数参数
  5. 执行函数体(可能包含一系列计算和对其他函数的调用)
  6. 记录返回值
  7. 跳转至调用方地址
  8. 获取函数返回值

如果使用固定的内存空间存储以上涉及的数据,将难以管理内存的释放从而造成内存空间浪费。尤其是当需要进行递归操作时,固定的内存空间难以被有效配置。而栈后进先出的特性刚好符合函数的调用和返回的需求。

ARM中,SP寄存器用于指向栈顶的内存地址,且栈将从一个较大的内存地址开始递减存储数据。栈有两种操作方式,PUSHPOP。PUSH即入栈,SP指针递减;POP即出栈,SP指针递增。

我们将使用栈进行存储:

  1. 函数参数
  2. LR寄存器的值(link register,存储外部调用方的地址用于函数返回后程序跳转继续执行)
  3. 计算过程中可能涉及的原寄存器中的值
  4. 函数返回结果
  5. 局部变量
  6. 任何其他临时的具有短暂生命周期的数据

当函数退出时,这些临时数据将被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跳转回调用方。PUSHPOP是两条伪指令,其作用是按照寄存器数值递减的顺序入栈或递增的顺序出栈,实际指令为STMFDLDMFD,这里不做额外解释。当需要分配或丢弃栈空间时,可直接对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)
...

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

A WindRunner. VoyagingOne

留言

您的电子邮箱地址不会被公开。 必填项已用*标注