ARM汇编(基于树莓派3B)2

第四章 控制程序流

无条件跳转

B label :最大允许跳转32MB范围

关于CPSR

在这里插入图片描述
条件标志位:
Negative(负数标志位): N is 1 if the signed value is negative andcleared if the result is positive or 0.
Zero(零标志位): Is set if the result is 0; this usually denotes an equal result from a comparison. If the result is non-zero, this flag is cleared.
Carry(进位标志位): For addition type operations, this flag is set if the result produces an overflow. For subtraction type operation, this flag is set if the result requires a borrow. Also, it’s used in shifting to hold the last bit that is shifted out.
• OVerflow(溢出标志位): For addition and subtraction, this flag is set if a signed overflow occurred. NOTE: Some instructions may specifically set oVerflow to flag an error condition.

中断标志位:
I: When set, disables IRQ interrupts(关闭IRQ中断)
F: When set, disables FIQ interrupts(关闭FIQ中断)
A: When set, disables imprecise aborts (关闭不精确的中止?)

指令集标志:
Thumb: 16-bit compact instructions(16位紧凑指令)
Jazelle: Obsolete mode for directly executing Java bytecodes(直接执行Java字节码的过时的模式)

其它标志位:
Q: This flag is set to indicate underflow and/or saturation.(设置该标志以指示下溢和/或饱和度)
GE: These flags control the Greater than or Equal behavior in SIMD instructions.
E: Is a flag that controls the “endianness” for data handling.(控制数据存储使用的大/小端模式)

M is the processor mode such as user or supervisor(处理器模式,例如用户模式、监视模式)

条件分支

一般的条件分支指令如下:
B{condition} label

{condition}FlagsMeaning
EQZ=1相等
NEZ=0不相等
CS或HSC=1无符号>=
CC或LOC=0无符号<
MIN=1负数
PLN=0正数或0
VSV=1溢出
VCV=0无溢出
HIC=1且Z=0无符号>
LSC=0且Z=1无符号<=
GEN=V有符号>=
LTN!=V有符号<
GTZ=0,N=V有符号>
LEZ=1,N!=V有符号<=
AL任何时候无条件执行

示例:
BEQ _start
当Z标志位为1时执行_start分支

Loops

for循环的汇编形式

@ 110
MOV R2, #1
loop: 
ADD R2, #1 @ I = I + 1
CMP R2, #10
BLE loop @ IF I <= 10 goto loop

while循环的汇编形式

loop: CMP R4, #5
BGE loopdone
... other statements in the loop body ...
B loop
loopdone: @program continues

If/Then/Else

IF R5 < 10 THEN
.... if statements ...
ELSE
... else statements ...
END IF

相应的汇编实现:

CMP R5, #10
BGE elseclause
... if statements ...
B endif
elseclause:
... else statements ...
endif: @ continue on after the /then/else ...

逻辑运算

AND{S} Rd, Rs, Operand2 与
EOR{S} Rd, Rs, Operand2 异或
ORR{S} Rd, Rs, Operand2 或
BIC{S} Rd, Rs, Operand2 (Rs AND NOT Operand2)

设计模式

在编写汇编语言代码时,很容易产生新的想法。例如,我们可以通过置寄存器的第十位为1,然后将其右移直到寄存器为零,来进行十次循环。这行得通,但是却使程序阅读变得困难。如果你退出程序并在下个月重新阅读该程序,你有可能对它抓狂。
设计模式是常见编程模式的典型解决方案。如果采用一些关于如何执行循环和其他编程结构的标准设计模式,它将使你阅读程序变得更加容易。
设计模式使编程更加高效,因为在大多数情况下,可以仅使用一系列经过实践检验的真实模式中的示例即可。
因此,我们以高级语言的模式实现了循环以及if / then / else。如果这样做,它将使我们的程序更可靠,更快速地编写。稍后,我们将研究如何使用汇编器中的宏来解决此问题。

将寄存器的内容存储到内存

STRB R6, [R1]

分支程序的性能

在第1章“入门”中,我们提到了ARM32指令集是在指令管道中执行的。
一条指令需要三个时钟周期来执行,每个周期需要1个时钟周期。
1.将指令从内存加载到CPU。
2.解码指令。
3.执行指令。
但是,CPU一次可以处理3条指令,每条指令执行不同的步骤,因此平均而言,每个时钟周期我们执行一条指令。但是当我们有分支时会发生什么呢?
执行分支时,我们已经解码了下一条指令并将指令2加载到前面。当我们执行分支时,我们将前面已经完成的工作丢弃并重新开始。这意味着分支之后的指令将需要三个时钟周期来执行。
如果在代码中设置了很多分支,则可能会降低性能,可能会使程序减慢3倍。另一个问题是,如果使用很多分支进行编程,则会导致产生意大利面条式代码,即所有代码行像一锅意大利面一样纠结在一起,很难维护。

更多的比较指令

• CMN Rn, Operand2 :使用加法而不是减法
• TEQ Rn, Operand2 : 在Rn和Operand2之间执行按位异或运算。它根据结果更新CPSR。
• TST Rn, Operand2 : 在Rn和Operand2之间执行按位与运算。它根据结果更新CPSR。

第五章 内存

ARM32使用所谓的负载存储架构(load-store architecture)。这意味着该指令集分为两类:一类用于从存储器中加载和存储值以及将值存储到存储器中,另一类用于在寄存器之间执行算术和逻辑运算。我们大部分时间都在研究算术和逻辑运算。现在,我们看看其他类。
ARM32指令集具有一些访问内存的强大指令,包括几种用于访问数组形式的数据结构以及在加载或存储数据时递增循环中的指针的技术。

定义内存内容

在加载和存储内存之前,首先我们需要定义一些要操作的内存。 GNU汇编器包含一些指令,可用来定义要在程序中使用的内存。这些出现在程序的.data部分中。下面的例子讲述了如何定义字节,字和ASCII字符串。

label: .byte 74, 0112, 0b00101010, 0x4A, 0X4a, 'J', 'H' + 2
.word 0x1234ABCD, -1434
.ascii "Hello World\n"

.byte语句定义1个或多个字节的内存。第一行定义了具有相同值的7个字节。我们可以用十进制,八进制,二进制,十六进制或ASCII定义字节。在定义数字的任何地方,我们都可以使用汇编器在编译程序时对其求值的表达式。.word和.ascii语句分别定义字数据和ASCII字符串。

下面列出了可用于每个字节内容的各种格式:

  • 以非零数字开头的十进制整数,包含十进制数字0-9。
  • 以0开始的八进制整数,包含八进制数字0-7。
  • 以0b或0B开头的二进制整数,包含二进制数字0–1。
  • 以0x或0X开头的十六进制整数,包含十六进制数字0–F。
  • 以0f或0e开头的浮点数,后跟一个浮点数。

注意:十进制数不要以零(0)开头,因为会被识别成八进制数。

我们可以在整数前面放置两个前缀运算符:

  • 取负(-)将取整数的补码。
  • 取反(~)将取整数的反码。

内存定义伪指令列表

伪指令解释
.ascii由双引号包围的字符串
.asciz以\0终止的ASCII字符串
.byte1字节整数
.double双精度浮点值
.float浮点值
.octa16字节整数
.quad8字节整数
.short2字节整数
.word4字节整数

如果要定义更大的内存集,则有两种机制可以执行此操作,而无需列出所有内存并对其进行计数,例如:

  • .fill repeat, size, value

这将重复给定大小的值,重复次数为repeat,例如:
zeros: .fill 10, 4, 0
创建一个内存块,该内存块包含10个4字节的字内存单元,所有字均为0。
以下代码
.rept count
. . .
.endr
在.rept和.endr之间重复语句count次。这其中包括任何代码,例如,可以重复count次循环,例如:

rpn: .rept 3
.byte 0, 1, 2
.endr

被解释为:

.byte 0, 1, 2
.byte 0, 1, 2
.byte 0, 1, 2

在ASCII字符串中,我们看到了特殊字符“ \ n”用来换行。还有一些常见的不可见字符,也使我们能够在字符串中加上双引号。 “ \”称为转义字符,它是定义特殊情况的元字符。

转义字符序列解释
\b退格(ASCII码8)
\f换页(ASCII码12)
\n换行(ASCII码10)
\r返回(ASCII码13)
\t制表符(ASCII码9)
\ddd八进制ASCII码(例如\123)
\xdd十六进制ASCII码(例如\x4F)
\\字符“\”
\"双引号字符
\anything-elseanything-else

加载寄存器

我们使用LDR将地址加载到寄存器中和加载该地址指向的数据。有一些方法可以在内存中建立索引,并支持所有技巧,以尽可能地利用我们的32位指令。

PC相对寻址
LDR R1, =helloworld

反汇编后为

LDR r1, [pc, #20]

在这里,我们的指令将helloworld字符串的地址加载到R1中。汇编器此时知道程序计数器的值,因此可以为正确的内存地址提供一个偏移量。因此,这称为PC相对寻址。
上面的偏移量在指令中占12位,范围为0–4095。指令中还有1位用来说明偏移的方向,因此我们获得了±4095的范围。在这种情况下,我们正在加载一个字(内存地址的大小),因此地址范围为±4095个字。
该指令的一般形式是:

LDR{type} Rt, =label

type是以下类型表中的一个:

类型意义
B无符号字节
SB有符号字节
H无符号半字(16位)
SH有符号半字(16位)
缺省情况为字

在这种简单的情况下,我们仅加载地址,type告诉我们的唯一内容就是数据的大小。如果我们加载一个字节,则偏移量将以字节为单位。稍后我们将看到,this signed part在加载和保存数据时非常重要。

PC相对寻址还有另外一个技巧。它提供了一种仅用一条指令就可以将任何32位的量加载到寄存器中的方法,例如

LDR R1, =0x1234ABCDF

这汇编成:

ldr r1, [pc, #8]
.word 0x1234abcd

GNU汇编器通过将所需的常量放入内存,然后创建PC相对指令进行加载。
在第2章“加载和添加”中,我们使用MOV / MOVT对进行了此操作。在这里,我们用一条指令做同样的事情。两者都花费同样的空间,即两条32位的指令或一条32位的指令加上一个32位的内存空间。
实际上,这就是汇编程序处理所有数据标号的方式。当我们指定

LDR R1, =helloworld

汇编器做了同样的事情:
它在内存中创建一个保存hello字符串的地址的空间,然后加载那个空间保存的内容,而不是直接加载helloworld本身代表的地址。
汇编程序创建的这些常量位于 .text部分的末尾,这是汇编指令所在的位置,而不在 .data中。
这使得它们在正常情况下为只读,因此无法对其进行修改。要修改的任何数据都应放在.data中。

汇编程序为什么要这样做?为什么不直接将PC相对索引指向数据呢?造成这种情况的原因有很多,并非所有原因都特定于ARM32指令集:

  1. 偏移量4096并不是很大,尤其是当有多个大字符串时。这样,我们可以访问4096个对象而不是4096个字(地址)。随着程序的不断扩大,这有助于使我们的程序保持同等的效率。
  2. 我们定义的所有标签都进入目标文件的符号表,从而使该地址数组实质上就是我们的符号表。这样,链接器/加载器和操作系统很容易更改内存地址,而无需重新编译程序。
  3. 如果需要将这些变量中的任何一个设置为全局变量,则只需将它们设置为全局变量(其他文件即可访问),而无需更改程序。如果我们没有这种间接的级别,那么将变量设为全局变量将需要对加载和保存它的指令进行调整。

个人理解:程序编译后,地址会成为指令的一部分,如果直接将数据的地址作为指令的一部分,那么当地址发生变化时,程序要重新编译生成新的指令,而如果我们使用一块内存来保存那个数据的地址,并让指令中的地址部分为这块内存的地址,那么当数据的地址变化时,只需要更改那块内存中的内容即可而无需重新编译程序。
这是帮助我们的工具的另一个示例,尽管起初似乎并非如此。在我们简单的单行示例中,它似乎增加了一层复杂性,但是在实际程序中,这是有效的设计模式。

从内存加载

在HelloWorld程序中,我们只需要将该地址传递给Linux,然后使用它来打印我们的字符串。通常,我们喜欢使用这些地址将数据加载到寄存器中。

@ load the address of mynumber into R1
LDR R1, =mynumber
@ load the word stored at mynumber into R2
LDR R2, [R1] 
.data
mynumber: .WORD 0x1234ABCD

如果在调试器中单步调试,则可以看到它将0x1234ABCD加载到R2中。
方括号表示间接内存访问。这意味着加载存储在R1指向的地址上的数据,而不是将R1的内容移到R2中。
当我们遇到“ LDR r1,[pc,#20]”时,看起来好像我们正在加载pc + 20的地址,但是现在我们知道我们实际上正在加载存储在pc + 20里的地址,这就是为什么方括号被使用。
请注意,如果要从此存储位置加载字节,需要在上面两条指令中都添加类型,否则将导致长度不匹配,并且不会加载您正在考虑的字节。
上面的做法是没问题的,但是你可能对于需要两条指令来从内存中加载值到R2不满:一条加载地址,然后一条加载数据。这是对RISC处理器进行终身编程的过程。每条指令执行速度非常快,但执行的工作量很小。在开发算法时,我们会看到通常会先加载一次地址,然后再大量使用它,因此,大多数访问操作都只执行一条指令。

通过内存建立索引

所有高级编程语言都具有数组结构。他们可以定义一个对象数组,然后通过索引访问各个元素。
ARM32指令集为我们提供了执行此类操作的支持。假设我们有一个由10个字元素组成的数组:
arr1: .FILL 10, 4, 0
让我们将数组的地址加载到R1中:

LDR R1, =arr1

现在,我们可以使用LDR访问元素:

@ Load the first element
LDR R2, [R1]
@ Load element 3
@ The elements count from 0, so 2 is the third one. 
@ Each word is 4 bytes, so we need to multiply by 4
LDR R2, [R1, #(2 * 4)]

在这里插入图片描述
这可以很好地访问硬编码的元素,但是通过变量又如何呢?

@ The 3rd element is still number 2
MOV R3, #(2 * 4)
@ Add the offset in R3 to R1 to get our element.
LDR R2, [R1, R3]

我们可以反向变换。如果R2指向数组的末尾,我们可以

LDR R2, [R1, #-(2 * 4)]
或者
LDR R2, [R1, -R3]

回想一下第二章中提到的Operand2,它的第一种类型为“寄存器和位移”,即寄存器后再加上位移指令,对上面的例子而言,前一个数是常数,那么我们可以直接对它乘上另一个常数,如上面的2 * 4,但若是前面的数是寄存器,那么我们就必须用位移运算来实现乘法:

@ Suppose our array is of WORDs but we only want the low order byte.
MOV R3, #2
@ Shift R3 left by 2 positions to multiply by 4 to get the correct address.
LDR R2, [R1, R3, LSL #2]
回写

如果通过加法和移位来计算地址,那么在我们加载寄存器后,结果将被丢弃。执行循环时,保留计算出的地址很方便。这样可以节省我们在索引寄存器上执行单独的ADD的操作。
语法是在指令后加上一个感叹号(!),然后汇编器将设置所生成指令中的位,要求CPU保存计算出的地址,因此

LDR R2, [R1, R3, LSL #2]!

会用计算出的值更新R1。在我们研究的示例中,此功能没有太大用处,但在下一节中将变得更加有用。

后变址寻址(Post-indexed Addressing)

前面的内容介绍了前变址寻址pre-indexed addressing)。这是因为先计算了地址,然后使用计算出的地址检索数据。在后变址寻址中,首先使用基址寄存器检索数据,然后完成偏移量的移位和加法运算。在一条指令的上下文中,这似乎很奇怪,但是当我们编写循环时,我们将看到这就是我们想要的。计算出的地址将回写到基址寄存器,否则的话使用此功能没有任何意义。
我们表示希望通过将要添加的项添加到方括号之外来进行后变址寻址。在以下的示例中,LDR将向R1加载R2指向的内存内容,然后使用每条指令中指示的方法更新R2。

@ Load R1 with the memory pointed to by R2
@ Then do R2 = R2 + R3
LDR R1, [R2], R3
@ Load R1 with the memory pointed to by R2
@ Then do R2 = R2 + 2
LDR R1, [R2], #2
@ Load R1 with the memory pointed to by R2
@ Then do R2 = R2 + (R3 shifted 2 left)
LDR R1, [R2], R3, LSL #2

以后变址寻址如何帮助我们编写循环为例,让我们考虑循环遍历一串ASCII字节。假设我们要将任何小写字符转换为大写。

//伪代码
i = 0
DO
char = instr[i]
IF char >= 'a' AND char <= 'z' THEN
char = char - ('a' - 'A')
END IF
outstr[i] = char
i = i + 1
UNTIL char == 0
PRINT outstr

在此示例中,我们将使用以NULL终止的字符串。这些在C编程中非常常见。在这里,字符串不是一个定长的字符序列,而是一个字符序列后跟一个NULL(ASCII代码0或\0)字符。要处理该字符串,我们只需循环直到遇到NULL字符。

@ Assembler program to convert a string to all uppercase.
@ R0-R2 - parameters to Linux function services
@ R3 - address of output string
@ R4 - address of input string
@ R5 - current character being processed
@ R7 - Linux function number
.global _start @ Provide program starting address
_start: LDR R4, =instr @ start of input string
LDR R3, =outstr @ address of output string

@ The loop is until byte pointed to by R1 is non-zero
@ Load character and increment pointer
loop: LDRB R5, [R4], #1
@ If R5 > 'z' then goto cont
CMP R5, #'z' @ is letter > 'z'?
BGT cont
@ Else if R5 < 'a' then goto end if
CMP R5, #'a'
BLT cont @ goto to end if
@ if we got here then the letter is lower-case,
@ so convert it.
SUB R5, #('a'-'A')
cont: @ end if
STRB R5, [R3], #1 @ store character to outstr
CMP R5, #0 @ stop on hitting a null char
BNE loop @ loop if character isn't null

@ Set up the parameters to print our hex number
@ and then call Linux to do it.
MOV R0, #1 @ 1 = StdOut
LDR R1, =outstr @ string to print
@ get the length by subtracting the pointers
SUB R2, R3, R1
MOV R7, #4 @ linux write system call
SVC 0 @ Call linux to output the string

@ Set up the parameters to exit the program
@ and then call Linux to do it.
MOV R0, #0 @ Use 0 return code
MOV R7, #1 @ Service command code 1
SVC 0 @ Call linux to terminate
.data
instr: .asciz "This is our Test String that we will convert.\n"
outstr: .fill 255, 1, 0

在此示例中,我们使用LDRB和STRB指令,因为我们正在逐字节处理。 STRB指令与LDRB指令相反。它将其第一个参数的内容保存到根据其所有其他参数构建的地址。
上面代码的反汇编:

Contents of section .text:
00010074 <_start>:
10074: e59f4044 ldr r4, [pc, #68] ; 100c0 <cont+0x2c>
10078: e59f3044 ldr r3, [pc, #68] ; 100c4 <cont+0x30>

0001007c <loop>:
1007c: e4d45001 ldrb r5, [r4], #1
10080: e355007a cmp r5, #122 ; 0x7a
10084: ca000002 bgt 10094 <cont>
10088: e3550061 cmp r5, #97 ; 0x61
1008c: ba000000 blt 10094 <cont>
10090: e2455020 sub r5, r5, #32

00010094 <cont>:
10094: e4c35001 strb r5, [r3], #1
10098: e3550000 cmp r5, #0
1009c: 1afffff6 bne 1007c <loop>
100a0: e3a00001 mov r0, #1
100a4: e59f1018 ldr r1, [pc, #24] ; 100c4 <cont+0x30>
100a8: e0432001 sub r2, r3, r1
100ac: e3a07004 mov r7, #4
100b0: ef000000 svc 0x00000000
100b4: e3a00000 mov r0, #0
100b8: e3a07001 mov r7, #1
100bc: ef000000 svc 0x00000000
100c0: 000200c8 .word 0x000200c8
100c4: 000200f7 .word 0x000200f7

Contents of section .data:
200c8 54686973 20697320 6f757220 54657374 This is our Test
200d8 20537472 696e6720 74686174 20776520 String that we
200e8 77696c6c 20636f6e 76657274 2e0a0000 will convert....
200f8 00000000 00000000 00000000 00000000 ................

指令

LDR R4, =instr

被转化为

ldr r4, [pc, #68] ; 100c0

注释告诉我们pc + 68是地址0x100c0。这说明pc此刻的值是0x1007c,即这条指令后的第2条指令的地址。为什么呢?想想前面提到过执行指令的三个流程:取指、译指、执行,并且ARM架构可以同时处理三条指令,每条指令处理的流程都不同,也就是说,对于当前正在执行的指令,此刻,处理器正在对它的下一条指令译指,对它的下下条指令进行取指,而PC里存放的是“正在取指的指令的地址”,所以这里的PC就是1007c。

双寄存器

我们已经看到了所有LDR和STR指令的双字版本。 LDRD指令将两个寄存器作为参数加载,然后将64位内存加载到其中。同样对于STRD指令。
例如,下面的例子加载双字元素的地址(仍然是32位),然后将该双字元素加载到R2和R3中。然后,我们将R2和R3存储回mydword中。

LDR R1, =mydword
LDRD R2, R3, [R1]
STRD R2, R3, [R1]
.data
mydword: .DWORD 0x1234567887654321

总结

通过本章,我们现在可以从内存中加载数据,在寄存器中对其进行操作,然后将结果保存回内存。
我们研究了数据加载和存储指令如何帮助我们处理数据数组,以及它们如何帮助我们循环遍历数据。
在下一章中,我们将研究如何使代码可重用。毕竟,如果可以随时调用我们的程序,会不会很方便?

第六章 函数和栈

当Raspbian运行一个程序时,它会给它分配一个8 MB大小的栈。在第1章“入门”中,我们提到了寄存器R13作为堆栈指针(SP)具有特殊用途。你可能已经注意到R13gdb中被命名为SP,并且你可能已经注意到在调试程序时,它具有很大的值,例如0x7efff380。这是指向当前堆栈位置的指针。
ARM32指令集具有两种操作堆栈的指令:多加载(LDM)和多存储(STM)。这两个指令有很多可选项。这是为了实现诸如堆栈是通过增加地址还是减少地址来增长,SP是否指向堆栈末尾或堆栈上的下一个空闲位置之类的事情。如果要创建自己的堆栈,或者要满足其他操作系统的要求,这些选项可能很有用。
幸运的是,GNU汇编器提供了更简单的伪指令,这些伪指令被映射回正确的LDM和STM形式。它们是

PUSH {reglist}
POP {reglist}

{reglist} 参数是一个寄存器列表,包含用逗号分隔的寄存器列表和寄存器范围。寄存器范围类似于R2-R4,这意味着R2,R3和R4,例如:

PUSH {r0, r5-r12}
POP {r0-r4, r6, r9-r12}

寄存器以数字顺序存储在堆栈中,最低的寄存器位于最低的地址。不应在此列表中包括PC或SP。
Pushing R5 onto the stack
Popping R4 from the stack

用连接寄存器实现跳转

要调用函数,我们需要让被调函数执行完后可以返回到主调函数中的调用函数指令的下一条指令的位置。我们使用连接寄存器(LRR14来完成它。要使用LR,我们介绍BL指令,它和分支指令B是一样的,不同之处在于它在执行分支之前将下一条指令的地址放入LR,从而为我们提供了一种机制从函数返回。
要从函数中返回,我们使用BX指令,该分支指令将一个寄存器作为参数,函数完成后,我们可以跳转到LR中存储的地址以继续之前的过程。
在下面的例子中,BL指令将以下MOV指令的地址存储到LR中,然后跳转到myfunc。 Myfunc会完成该函数的工作,然后通过BX分支跳转到LR中存储的位置(这是BL指令之后的MOV指令)将执行权还给调用者。

@ ... other code ...
BL myfunc
MOV R1, #4
@ ... more code ...
-----------------------------
myfunc: @ do some work
BX LR

嵌套函数调用

我们成功地从函数中调用并返回到函数,但从未使用过堆栈。为什么我们先引入堆栈的概念却不使用它?首先考虑一下,如果myfunc在处理过程中调用另一个函数会发生什么。如果myfunc执行BL指令,则BL会将下一个地址复制到LR中,从而覆盖myfunc的返回地址,导致myfunc无法返回。我们需要的是一种在函数之间调用函数时保持返回地址链的方法。好吧,不是一连串的地址链,而是地址栈。
如果myfunc要调用其他函数,则它首先需要将LR压栈,并在返回之前将其从堆栈中弹出,例如,

@ ... other code ...
BL myfunc
MOV R1, #4
@ ... more code ...
-----------------------------
myfunc: PUSH {LR}
@ do some work ...
BL myfunc2
@ do some more work...
POP {LR}
BX LR
myfunc2: @ do some work ....
BX LR

函数参数和返回值

调用者在R0R1R2R3中传递前四个参数。如果还有其他参数,则将它们压入堆栈。如果只有两个参数,则仅使用R0R1。这意味着前四个参数已经加载到寄存器中,可以进行处理了。在处理之前,需要从堆栈中弹出其他参数。
要将结果返回给调用者,在返回之前将其放在R0中。如果需要返回更多数据,则可以把存储位置的地址作为参数之一,在其中放置要返回的其他数据。
管理寄存器

管理寄存器

调用一个函数,该函数很可能是由其他程序员编写的,并且你不知道它将使用什么寄存器。如果每次调用函数都必须重新加载所有寄存器,那将是非常低效的。因此,有一组规则来控制函数可以使用哪些寄存器以及哪些寄存器需要被保存:

  • R0–R3:这些是函数参数。函数可以将它们用于任何其他目的,自由地对其进行修改。如果调用例程需要保存它们,则必须保存它们本身。
  • R4–R12:可以由被调例程自由使用它们,除非需要保存它们的话。这意味着调用例程可以假定这些寄存器是不受影响的。
  • SP:被调例程可以自由使用。该例程必须按其按入栈次数相同的次数来弹栈,因此对于调用例程而言,它是完整的。
  • LR:被调例程必须维护好它,正如我们在上一节中讨论的那样。
  • CPSR:这两个例程都不能对CPSR做出任何假设。就所调用的例程而言,所有标志都是未知的。同样,当函数返回时,它们也是未知的。

函数调用总结

主调例程
1.如果我们需要R0–R4中的任何一个,请保存它们。
2.将前四个参数移入寄存器R0–R4中。
3.将所有其他参数压入堆栈。
4.使用BL调用函数。
5.得到R0中的返回值。
6.恢复我们保存的R0–R4中的任何一个。

被调函数
1.将LR和R4-R12压栈。
2.做其它工作。
3.将我们的返回值放入R0。
4. POP LR和R4-R12。
5.使用BX指令将执行权返回给调用方。

再谈大写

让我们将第5章中的大写示例组织为适当的函数。我们将函数移到它自己的文件中,并修改makefile以同时生成调用程序和大写函数。
首先创建一个名为main.s的文件,其中包含用于驱动应用程序。

@
@ Assembler program to convert a string to
@ all uppercase by calling a function.
@
@ R0-R2 - parameters to linux function services
@ R1 - address of output string
@ R0 - address of input string
@ R5 - current character being processed
@ R7 - linux function number
@

.global _start @ Provide program starting address
_start: LDR R0, =instr @ start of input string
LDR R1, =outstr @ address of output string
BL toupper

@ Set up the parameters to print our hex number
@ and then call Linux to do it.
MOV R2,R0 @ return code is the length of the string
MOV R0, #1 @ 1 = StdOut
LDR R1, =outstr @ string to print
MOV R7, #4 @ linux write system call
SVC 0 @ Call linux to output the string

@ Set up the parameters to exit the program
@ and then call Linux to do it.
MOV R0, #0 @ Use 0 return code
MOV R7, #1 @ Command code 1 terminates
SVC 0 @ Call linux to terminate the program

.data
instr: .asciz "This is our Test String that we will
convert.\n"
outstr: .fill 255, 1, 0

现在创建一个名为upper.s的文件,其中包含大写转换函数。

@
@ Assembler program to convert a string to
@ all uppercase.
@
@ R1 - address of output string
@ R0 - address of input string
@ R4 - original output string for length calc.
@ R5 - current character being processed
@

.global toupper @ Allow other files to call this routine
toupper: PUSH {R4-R5} @ Save the registers we use.
MOV R4, R1
@ The loop is until byte pointed to by R1 is non-zero
loop: LDRB R5, [R0], #1 @ load character and increment pointer
@ If R5 > 'z' then goto cont
CMP R5, #'z' @ is letter > 'z'?
BGT cont
@ Else if R5 < 'a' then goto end if
CMP R5, #'a'
BLT cont @ goto to end if
@ if we got here then the letter is lower case, so convert it.
SUB R5, #('a'-'A')
cont: @ end if
STRB R5, [R1], #1 @ store character to output str
CMP R5, #0 @ stop on hitting a null
character
BNE loop @ loop if character isn't null
SUB R0, R1, R4 @ get the length by subtracting the
pointers
POP {R4-R5} @ Restore the register we use.
BX LR @ Return to caller

要构建这些文件,请使用makefile。

UPPEROBJS = main.o upper.o
ifdef DEBUG
DEBUGFLGS = -g
else
DEBUGFLGS =
endif
LSTFLGS =
all: upper
%.o : %.s
as $(DEBUGFLGS) $(LSTFLGS) $< -o $@
upper: $(UPPEROBJS)
ld -o upper $(UPPEROBJS)

栈帧

在大写转化函数中,我们不需要任何额外的内存,因为我们可以使用可用的寄存器来完成所有工作。当我们编写更大的函数时,我们经常需要为变量提供更多的内存,而不是在寄存器中。我们不在 .data中添加混乱,而是将这些变量存储在堆栈中。
将这些变量压入堆栈是不切实际的,因为我们通常需要以随机顺序访问它们,而不是强制执行PUSH / POP的严格LIFO规定。
为了在栈上分配空间,我们使用减法指令将栈增长所需的数量。假设我们需要三个变量,每个变量都是32位整数,例如a,b和c。因此,我们需要在堆栈上分配12个字节(3个变量x 4个字节/字)。

SUB SP, #12

这会将堆栈指针向下移动12个字节,从而在堆栈上为我们提供了一个存储变量的区域。假设a在R0中,b在R1中,c在R2中,那么我们可以使用

STR R0, [SP] @ Store a
STR R1, [SP, #4] @ Store b
STR R2, [SP, #8] @ Store c

在函数结束之前,我们需要执行

ADD SP, #12

从堆栈中释放变量。请记住,函数的责任是在返回SP之前将SP恢复到原始状态。
这是分配一些变量的最简单方法。但是,如果我们在函数中使用堆栈来做很多其他事情,则很难跟踪这些偏移量。我们缓解这种情况的方法是使用栈帧。在这里,我们在栈上分配一个区域,并将指向该区域的指针保存在另一个称为帧指针FP)的寄存器中。您可以使用任何寄存器作为FP,但是我们将遵循C编程约定并使用R11
要使用栈帧,我们首先将帧指针设置为栈上的下一个空闲位置(它以降序地址增长),然后像以前一样分配空间:

SUB FP, SP, #4
SUB SP, #12

现在,我们使用FP的偏移量处理变量:

STR R0, [FP] @ Store a
STR R1, [FP, #-4] @ Store b
STR R2, [FP, #-8] @ Store c

当使用FP时,我们需要将其包括在函数开头的PUSH寄存器列表中,然后将其包括在POP末尾。从R11开始,FP是我们需要保存的。
我们倾向于不使用FP。这样就节省了函数进入和退出的几个周期。毕竟,在汇编语言编程中,我们希望提高效率。

使大写循环成为可重用的代码段的另一种方法是使用宏。 GNU汇编器具有强大的宏功能。汇编程序使用宏而不是调用函数,而是在每个被调用的位置创建代码副本,以替换任何参数。考虑一下我们的大写程序的替代实现;第一个文件是mainmacro.s。

@
@ Assembler program to convert a string to
@ all uppercase by calling a macro.
@
@ R0-R2 - parameters to linux function services
@ R1 - address of output string
@ R0 - address of input string
@ R7 - linux function number
@

.include "uppermacro.s"
.global _start @ Provide program starting address
_start: toupper tststr, buffer

@ Set up the parameters to print our hex number
@ and then call Linux to do it.
MOV R2,R0 @ R0 is the length of the string
MOV R0, #1 @ 1 = StdOut
LDR R1, =buffer @ string to print
MOV R7, #4 @ linux write system call
SVC 0 @ Call linux to output the string
@ Call it a second time with our second string.
toupper tststr2, buffer

@ Set up the parameters to print our hex number
@ and then call Linux to do it.
MOV R2,R0 @ R0 is the length of the string
MOV R0, #1 @ 1 = StdOut
LDR R1, =buffer @ string to print
MOV R7, #4 @ linux write system call
SVC 0 @ Call linux to output the string

@ Set up the parameters to exit the program
@ and then call Linux to do it.
MOV R0, #0 @ Use 0 return code
MOV R7, #1 @ Service command code 1 terminates
this program
SVC 0 @ Call linux to terminate

.data
tststr: .asciz "This is our Test String that we will convert.\n"
tststr2: .asciz "A second string to uppercase!!\n"
buffer: .fill 255, 1, 0

字符串大写的宏在uppermacro.s中:

@
@ Assembler program to convert a string to
@ all uppercase.
@
@ R1 - address of output string
@ R0 - address of input string
@ R2 - original output string for length calc.
@ R3 - current character being processed
@
@ label 1 = loop
@ label 2 = cont

.MACRO toupper instr, outstr
LDR R0, =\instr
LDR R1, =\outstr
MOV R2, R1
@ The loop is until byte pointed to by R1 is non-zero
1: LDRB R3, [R0], #1 @ load character and increment
pointer
@ If R5 > 'z' then goto cont
CMP R3, #'z' @ is letter > 'z'?
BGT 2f
@ Else if R5 < 'a' then goto end if
CMP R3, #'a'
BLT 2f @ goto to end if
@ if we got here then the letter is lower-case, so convert it.
SUB R3, #('a'-'A')
2: @ end if
STRB R3, [R1], #1 @ store character to output str
CMP R3, #0 @ stop on hitting a null character
BNE 1b @ loop if character isn't null
SUB R0, R1, R2 @ get the length by subtracting the pointers
.ENDM
Include

文件uppermacro.s定义了我们的宏,可将字符串转换为大写。宏不会生成任何代码;它只是为汇编程序定义了宏,以便在调用它的位置插入。该文件不会生成目标(* .o)文件;而是无论哪个文件需要使用它,它都被包括在内。

.include "uppermacro.s"

获取此文件的内容并将其插入到此处,以便我们的源文件变大。这是在进行任何其他处理之前完成的。这类似于C #include预处理程序指令。

宏定义

宏是使用 .MACRO指令定义的。它给出了宏的名称并列出了其参数。宏在 .ENDM指令处结束。指令的形式是

.MACRO macroname parameter1, parameter2, ...

在宏内,在参数名前加上反斜杠(例如\parameter1)来指定参数1的值,从而指定参数。我们的toupper宏定义了两个参数instr和outstr。
你可以看到如何在有\instr和\oustr的代码中使用参数。这些是文本替换,需要使用正确的Assembly语法,否则会出现错误。

标号

我们的标签“ loop”和“ cont”被标签“ 1”和“ 2”代替。这不符合程序的可读性。我们这样做的原因是,如果不这样做,那么如果我们多次使用宏,则会得到一个错误,即标签被多次定义。这里的技巧是,汇编程序使您可以定义数字标签任意多次。然后在我们的代码中引用它们,我们使用

BGT 2f
BNE 1b @ loop if character isn't null

2之后的f表示正向的下一个标签21b表示反向的下一个标签1
为了证明这是可行的,我们在mainmacro.s文件中两次调用toupper,以显示一切正常,并且我们可以根据需要多次重复使用此宏。

为什么要使用宏?

宏在每次使用时都会替换代码的副本。这将使您的可执行文件更大。如果你执行

objdump -d mainmacro

你将看到插入的两个代码副本。使用函数,每次都不会生成额外的代码。这就是即使在处理堆栈方面有额外的工作,函数也很吸引人的原因。
使用宏的原因是性能。大多数Raspberry Pi型号具有1 GB或更多的内存,可以容纳大量代码。记住,每当执行分支时,都必须重新启动执行管道,从而使分支跳转成为昂贵的指令。使用宏,我们省去了BL分支来调用函数和BX分支来返回。我们还省去了PUSH和POP指令以保存和恢复我们使用的所有寄存器。如果宏很小并且我们经常使用它,则可能会节省大量执行时间。

总结

在本章中,我们介绍了ARM堆栈以及如何使用它来帮助实现函数。我们介绍了如何编写和调用函数,这是创建可重用代码库的第一步。我们了解了如何管理寄存器的使用情况,因此我们的调用程序和函数之间没有任何冲突。我们学习了函数调用协议,该协议将使我们能够与其他编程语言进行互操作。
我们研究了为局部变量定义基于堆栈的存储以及如何使用此内存。
最后,我们介绍了GNU汇编器的宏功能,作为某些有性能上的要求的应用程序中函数的替代方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值