底层原理,  技术人生

ARM汇编基础:控制结构

控制结构包括判断、循环等。本文通过程序实例讲解ARM汇编控制结构的使用,并在必要时优化代码提升执行效率。

ARM版本:ARM V7 32位

编译环境:Perentie(UoM独占)

操作系统:Linux Mint

第一部分 打招呼

程序功能:首先输出”hello “,然后允许用户输入姓名(任意内容),最后输出”and good-bye!”。

为了实现这个程序,第一步我们需要在程序中存储字符串”Hello \0″和”and good-bye!\n\0″,用于在开头和结尾输出。注意字符串以’\0’结尾,对应的整数值为0,这一点与C语言特性类似,其目的是用于判断是否输出至字符串结尾。

程序中间部分通过循环获取用户字符输入,并检测是否收到回车字符(对应整数值为10),若获取到回车则跳出循环。

首先定义字符串变量。

hello	DEFB	"Hello \0"
goodbye	DEFB	"and good-bye!\n\0"
	ALIGN

DEFB指令用于逐字节写入内存空间,这里写入相应的字符串数据。ALIGN是一条编译器指示指令,使编译器将数据写入位置对齐至以字(32位)为单位的内存地址处。使程序能够正确读取接下来的32位指令。

在程序开头使用B命令无条件跳转至程序起始main处,并输出”hello “。

	B main
...
main	ADR	R0, hello	; printf("Hello ")
	SVC 	3

ADR指令将hello标签指向的内存地址存储至R0寄存器中,对应的内存中存储着字符串数据。”SVC 3″指令输出以R0中存储的内存地址开头的字符串至终端中。(Perentie独占指令,不同OS使用的SVC指令可能不同)

下面进入主循环体,获取用户输入并实时输出。

loop	CMP	R0, #10	
	BEQ	end		; while R0 != 10 {// translate to ARM code

	SVC	1		; input a character to R0
	SVC	0		; output the character in R0

	B	loop
end

首先判断是否获取到回车符,若R0 == 10则跳出循环。CMP为比较指令,它将计算R0 – #10,并根据计算结果更新标志位寄存器。BEQ为有条件跳转指令,它根据对应标志位寄存器的值判断条件,若满足指定条件则跳转,这里指若相等(CMP计算结果为0)则跳至end处。

“SVC 1″用于从终端获取单字符用户输入,并保存至R0寄存器中。”SVC 0″将R0寄存器中的单字符输出至终端。两条指令结合使用实现实时回显用户输入。

最后B指令无条件跳转至循环开头位置,执行循环体。

程序最后向用户输出”and good-bye!”。

	ADR	R0, goodbye 	; printf("and good-bye!")
	SVC	3

	SVC  	2		; stop the program

“SVC 2″指令用于结束程序执行。

Part 1 完整代码

; Hello Someone program - version 1 - 

	B main

hello	DEFB	"Hello \0"
goodbye	DEFB	"and good-bye!\n\0"
	ALIGN

main	ADR	R0, hello	; printf("Hello ")
	SVC 	3

loop	CMP	R0, #10	
	BEQ	end		; while R0 != 10 {

	SVC	1		; input a character to R0
	SVC	0		; output the character in R0

	B	loop
end		; }

	ADR	R0, goodbye 	; printf("and good-bye!")
	SVC	3

	SVC  	2		; stop the program
Part 1执行结果

第二部分 改进输出

第一部分已经基本实现了打招呼程序的功能,唯一美中不足的是末尾输出的”and good-bye!”会换行显示。这是由于我们在回显用户输入的同时,最后的回车符同样被回显了,在第二部分我们将改进并解决这个问题。

程序的主体结构和第一部分一致,唯一需要修改的是中间的循环体部分。

我们将在接受用户输入后避免直接回显,而首先进行判断。倘若输入字符不为回车则正常回显并执行循环体,反之不再回显并跳出循环体。

这里我们将略微调整程序结构,利用指令顺序执行的特性(PC寄存器总是+4),充分利用跳转语句的效果。结束部分的指令并没有放在程序末尾,而是在循环体代码之中。

Part 2 完整代码如下。

; Hello Someone program - version 2 - 

	B main

hello	DEFB	"Hello \0"
goodbye	DEFB	" and good-bye!\n\0"
	ALIGN

main	ADR	R0, hello	; print("Hello ")
	SVC 	3

next				; while (true) {
	SVC	1		; input a character to R0

	CMP	R0, #10
	BNE	loop		; if R0 == 10 do:

	ADR	R0, goodbye 	;   printf(" and good-bye!")
	SVC	3
	SVC  	2		;   stop the program

loop	; done // translate to ARM code

	SVC	0		; output the character in R0
	B	next		; } //while
Part 2 执行结果

第三部分 优化循环

在第一部分,我们在循环体中使用了两次跳转指令。一次用于判断回车符并跳出循环,第二次用于无条件跳转再次执行循环体。事实上,由于第二次跳转是无条件的,逻辑上近似于顺序执行指令,因此我们可以调整指令的执行顺序,在循环体的最后进行字符的判断并依据此,决断是否需要再次跳到开头执行循环。这样即可缩减一条跳转指令从而提高程序的运行效率。事实上,这也是一种针对循环的常用优化方法。

Part 3 循环体优化代码如下。

; Hello Someone program - version 3 - 

	B main

hello	DEFB	"Hello \0"
goodbye	DEFB	"and good-bye!\n\0"
	ALIGN

main	ADR	R0, hello	; printf("Hello ")
	SVC 	3

loop	; while R0 != 10 {

	SVC	1		; input a character to R0
	SVC	0		; output the character in R0

	CMP	R0, #10	
	BNE	loop		; }

	ADR	R0, goodbye 	; printf("and good-bye!")
	SVC	3

	SVC  	2		; stop the program

执行结果和Part 1相同。

第四部分 年龄变迁

从这个部分开始,我们将写一个新的程序来训练控制结构的使用。

程序功能:指定用户出生年份,输出该出生年份,并显示自出生年份起至今年每一年的年龄。

输出效果如图所示。

Part 4 预期结果

首先定义程序所需的相关数据,包括输出字符串,当前年份、出生年份等数值,和循环体中用到的临时年份、临时年龄等临时变量数值。

born	DEFB "you were born in \0"
were	DEFB "you were \0"
in	DEFB " in \0"
are	DEFB "you are \0"
this	DEFB " this year\n\0"
	ALIGN

present	DEFW	2020	; present
birth	DEFW	2000	; birth
year	DEFW	0	; year
age	DEFW	1	; age

定义程序开头主体部分,首先输出”you were born in ” + 用户出生年份,然后初始化各个寄存器的值,分别存储循环体中的所需数据。

	B  main
...
main
	; print "you were born in " + str(birth)
	ADR R0, born
	SVC 3
	LDR R0, birth
	SVC 4
	MOV R0, #10
	SVC 0

	LDR	R1, present
	LDR	R2, birth

	ADD	R3, R2, #1	; year = birth + 1
	STR	R3, year

	LDR	R4, age

LDR指令将标签指向的内存地址起始的32位内存数据值读取到对应的寄存器中。”SVC 4″指令将以整数形式输出R0中存储的数值。MOV指令用于给寄存器赋值,赋值的内容可以是寄存器中的值,被移位的寄存器中的值或立即数。这里MOV结合SVC使用用于输出回车符。

下面又用LDR指令将当前年份,出生年份和临时年龄分别加载到寄存器R1,R2,R4中。R3中存储的是临时年份,其初始值为出生年份+1。ADD指令将R2与立即数1的和存储至R3中,STR指令将R3中的32位值存储至year对应的内存中。注意R3和R4将作为临时变量在循环过程中迭代更新。

接下来进入循环体部分,程序循环输出每一个年份和对应的年龄。

loop	CMP	R3, R1
	BEQ	end		; while year != present //{

	; print "you were " + str(age) + " in " + str(year)
	ADR R0, were
	SVC 3
	LDR R0, age
	SVC 4
	ADR R0, in
	SVC 3
	LDR R0, year
	SVC 4
	MOV R0, #10
	SVC 0

	ADD	R3, R3, #1	;   year = year + 1
	ADD	R4, R4, #1	;   age = age + 1
	STR	R3, year
	STR	R4, age

	B	loop
end		; }

ADD指令用于在循环过程中更新年份和年龄,每次循环+1,并在每次更新后实时存储回内存中。程序在输出时使用LDR指令将对应内存中的变量值读取到R0寄存器中进行输出,同时需要输出的还有静态字符串(ADR读取字符串首地址)和回车符。循环退出的条件是临时年份等于当前年份,使用CMP配合BEQ指令,条件成立则跳出循环。

跳出循环后,程序最后输出今年的年龄并退出。

	; print "you are " + str(age) + "this year"
	ADR R0, are
	SVC 3
	LDR R0, age
	SVC 4
	ADR R0, this
	SVC 3

	SVC 2 ; stop

Part 4 完整代码如下。

; Age History - version 1 - 

	B  main

born	DEFB "you were born in \0"
were	DEFB "you were \0"
in	DEFB " in \0"
are	DEFB "you are \0"
this	DEFB " this year\n\0"
	ALIGN

present	DEFW	2020	; present
birth	DEFW	2000	; birth
year	DEFW	0	; year
age	DEFW	1	; age

main
	; print "you were born in " + str(birth)
	ADR R0, born
	SVC 3
	LDR R0, birth
	SVC 4
	MOV R0, #10
	SVC 0

	LDR	R1, present
	LDR	R2, birth

	ADD	R3, R2, #1	; year = birth + 1
	STR	R3, year

	LDR	R4, age

loop	CMP	R3, R1
	BEQ	end		; while year != present //{

	; print "you were " + str(age) + " in " + str(year)
	ADR R0, were
	SVC 3
	LDR R0, age
	SVC 4
	ADR R0, in
	SVC 3
	LDR R0, year
	SVC 4
	MOV R0, #10
	SVC 0

	ADD	R3, R3, #1	;   year = year + 1
	ADD	R4, R4, #1	;   age = age + 1
	STR	R3, year
	STR	R4, age

	B	loop
end		; }

	; print "you are " + str(age) + "this year"
	ADR R0, are
	SVC 3
	LDR R0, age
	SVC 4
	ADR R0, this
	SVC 3

	SVC 2 ; stop

第五部分 优化内存读写

第五部分将在第四部分的基础上继续优化代码。在第四部分中,每次更新临时变量并输出都会将寄存器中的计算结果先写入到内存中,再从内存中读取数据并完成输出。然而,内存读写是十分低效的。是否能在程序运行过程中避免频繁的内存操作,而尽量全部由寄存器读写完成呢?

我们将在程序开始执行时初始化所有变量,将所需的值读取到内存中。然后在整个程序运行过程中,不再涉及任何内存操作,仅通过寄存器更新并存储数值。

修改方法十分简单,将程序运行过程中的数据输出由LDR全部改为MOV来更新R0寄存器即可,再删除STR指令,这样就避免了使用内存。

; Age History - version 2 - 

	; initialize variables
	LDR	R4, present
	LDR	R5, birth
	LDR	R6, year
	LDR	R7, age

	B  main

born	DEFB "you were born in \0"
were	DEFB "you were \0"
in	DEFB " in \0"
are	DEFB "you are \0"
this	DEFB " this year\n\0"
	ALIGN

present	DEFW	2020	; present
birth	DEFW	2000	; birth
year	DEFW	0	; year
age	DEFW	1	; age

main
	; print "you were born in " + str(birth)
	ADR R0, born
	SVC 3
	MOV R0, R5
	SVC 4
	MOV R0, #10
	SVC 0

	ADD	R6, R5, #1	; year = birth + 1

	B	start		; Just in case someone was born in 2019
loop	; while year != present //{

	; print "you were " + str(age) + " in " + str(year)
	ADR R0, were
	SVC 3
	MOV R0, R7
	SVC 4
	ADR R0, in
	SVC 3
	MOV R0, R6
	SVC 4
	MOV R0, #10
	SVC 0

	ADD	R6, R6, #1	;   year = year + 1
	ADD	R7, R7, #1	;   age = age + 1

start	CMP	R6, R4	; optimize to use just one branch in the loop
	BNE	loop		; }

	; print "you are " + str(age) + "this year"
	ADR R0, are
	SVC 3
	MOV R0, R7
	SVC 4
	ADR R0, this
	SVC 3

	SVC 2 ; stop

类似于Part 3,我还优化了循环跳转指令,在循环体中减少了一条跳转指令。为了严谨性,在进入循环前使用无条件跳转至CMP,首先进行数据比较,避免直接输出。

程序运行结果和Part 4一致。

The End

A WindRunner. VoyagingOne

2条评论

留言

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