在第1章“入门”中,我们需要能够退出程序并显示字符串。我们使用Raspbian Linux来执行此操作,直接调用操作系统服务。在所有高级编程语言中,都有一个运行时库,其中包含用于调用操作系统的封装器。这些服务看起来就像是高级语言的一部分。在本章中,我们将研究这些运行时库在调用Linux的背后具体干什么以及向我们提供哪些服务。
解决软件不兼容性的方法通常是添加一个新函数。然后,旧函数将成为一个弱封装,将参数转换为新函数所需的参数。这样的示例是任何将文件或文件大小作为偏移量的文件访问例程。最初,32位Linux仅支持32位长度(4 GB)的文件。这太小了,并且添加了一组全新的文件I / O例程,这些例程采用64位参数表示文件的偏移量和大小。所有这些功能都类似于32位版本,但在名称后附加了64。
我们使用了两个系统调用:一个系统调用将ASCII数据写入控制台,第二个是退出程序。系统调用的调用约定与函数的调用约定不同。它使用软件中断将上下文从我们的用户级程序切换到Linux内核的上下文。
调用约定是:
对于我们而言,软件中断是一种聪明的方式,可以在Linux内核中调用例程,而无需知道它们在内存中的存储位置。它还提供了一种机制,可以在调用执行时以更高的安全级别运行。 Linux将检查是否具有执行请求的操作的正确访问权限,如果被拒绝,则会返回错误代码,如EACCES(13)。
尽管它不遵循第6章“函数和堆栈”中的函数调用约定,但是Linux系统调用机制将维护所有未用作参数或返回码的寄存器。当系统调用需要大量的参数时,它们倾向于将指向内存块的指针作为一个参数,然后将其保存所需的所有数据。因此,大多数系统调用不会使用那么多参数。
这些函数的返回码通常为零,如果成功则返回正数,如果失败则返回负数。负数是错误代码。例如,如果打开成功,打开文件的open调用将返回文件描述符。文件描述符是一个小的正数,如果失败,则为负数。
许多Linux服务将指向内存块的指针作为其参数。这些内存块的内容用C语言的结构体记录,因此,作为汇编程序员,我们必须对C进行逆向工程并复制内存结构。例如,nanosleep服务可使您的程序睡眠数纳秒。它被定义为
int nanosleep(const struct timespec *req, struct timespec *rem);
然后将struct timespec定义为
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
然后,我们必须清楚这是两个32位整数,然后在汇编语言中定义
timespecsec: .word 0
timespecnano: .word 100000000
要使用它们,我们将前两个参数的地址加载到寄存器中:
ldr r0, =timespecsec
ldr r1, =timespecnano
我们将在第8章“编程GPIO引脚”中使用nanosleep函数,但这是直接调用某些Linux服务所需的典型操作。
我们将开发一个例程或宏的库来简化我们的工作,而不是每次我们调用Linux服务时都弄清所有寄存器。C语言包含了用于所有Linux服务的函数调用的封装。我们将在第9章“与C和Python交互”中看到如何使用它们。
我们将使用GNU汇编器的宏开发Linux系统调用库,而不是重复C运行时库的工作。我们不会为所有函数开发此功能,只为我们需要的函数开发。大多数程序员都这样做,然后随着时间的推移,他们的库变得相当广泛。
宏的问题在于,你通常需要具有不同参数类型的多个变体。例如,有时您可能想使用寄存器作为参数来调用宏,而有时则使用立即数来调用宏。
在本章中,我们提供了一个完整的程序,可将文本文件的内容全部转换为大写。我们将使用第6章“函数与堆栈”中的toupper函数,并练习编写循环和if语句。
首先,我们需要一个文件I / O例程库从输入文件中读取,然后将大写版本写入另一个文件。如果你会C语言,那么这些看起来应该很熟悉,因为C运行时在这些服务上提供了一层简单的封装。我们创建一个文件fileio.s来执行此操作。
@ Various macros to perform file I/O
@ The fd parameter needs to be a register.
@ Uses R0, R1, R7.
@ Return code is in R0.
.include "unistd.s"
.equ O_RDONLY, 0
.equ O_WRONLY, 1
.equ O_CREAT, 0100
.equ S_RDWR, 0666
.macro openFile fileName, flags
ldr r0, =\fileName
mov r1, #\flags
mov r2, #S_RDWR @ RW access rights
mov r7, #sys_open
svc 0
.endm
.macro readFile fd, buffer, length
mov r0, \fd @ file descriptor
ldr r1, =\buffer
mov r2, #\length
mov r7, #sys_read
svc 0
.endm
.macro writeFile fd, buffer, length
mov r0, \fd @ file descriptor
ldr r1, =\buffer
mov r2, \length
mov r7, #sys_write
svc 0
.endm
.macro flushClose fd
@fsync syscall
mov r0, \fd
mov r7, #sys_fsync
svc 0
@close syscall
mov r0, \fd
mov r7, #sys_close
svc 0
.endm
现在我们需要一个主程序来协调流程。我们将其命名为main.s:
@
@ Assembler program to convert a string to all uppercase by calling a function.
@ R0-R2, R7 - used by macros to call linux
@ R8 - input file descriptor
@ R9 - output file descriptor
@ R10 - number of characters read
@
.include "fileio.s"
.equ BUFFERLEN, 250
.global _start @ Provide program starting address
_start: openFile inFile, O_RDONLY
MOVS R8, R0 @ save file descriptor
BPL nxtfil @ pos number file opened ok
MOV R1, #1 @ stdout
LDR R2, =inpErrsz @ Error msg
LDR R2, [R2]
writeFile R1, inpErr, R2 @ print the error
B exit
nxtfil: openFile outFile, O_CREAT+O_WRONLY
MOVS R9, R0 @ save file descriptor
BPL loop @ pos number file opened ok
MOV R1, #1
LDR R2, =outErrsz
LDR R2, [R2]
writeFile R1, outErr, R2
B exit
@ loop through file until done.
loop: readFile R8, buffer, BUFFERLEN
MOV R10, R0 @ Keep the length read
MOV R1, #0 @ Null terminator for string
@ set up call to toupper and call function
LDR R0, =buffer @ first param for toupper
STRB R1, [R0, R10] @ put null at end of string.
LDR R1, =outBuf
BL toupper
writeFile R9, outBuf, R10
CMP R10, #BUFFERLEN
BEQ loop
flushClose R8
flushClose R9
@ Set up the parameters to exit the program
@ and then call Linux to do it.
exit: MOV R0, #0 @ Use 0 return code
MOV R7, #1 @ Command code 1 terms
SVC 0 @ Call linux to terminate
.data
inFile: .asciz "main.s"
outFile: .asciz "upper.txt"
buffer: .fill BUFFERLEN + 1, 1, 0
outBuf: .fill BUFFERLEN + 1, 1, 0
inpErr: .asciz "Failed to open input file.\n"
inpErrsz: .word .-inpErr
outErr: .asciz "Failed to open output file.\n"
outErrsz: .word .-outErr
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)
该程序使用第6章“函数和堆栈”中的upper.s文件,其中包含大写转换的函数。该程序还使用Linux系统调用中的unistd.s,它给出了Linux服务功能编号的意义定义。
如果您生成此程序,请注意,它的大小仅为13 KB。这是纯汇编语言编程的魅力之一。程序没有任何额外的内容——我们控制每个字节——没有添加神秘的库。
Linux打开服务是Linux系统服务的典型代表。它包含三个参数:
返回码是文件描述符或错误代码。像许多Linux服务一样,该调用通过使错误返回码为负并使成功返回码为正,从而使该调用适合单个返回码。
书往往不会提倡错误检查的良好编程习惯。示例程序尽可能地小,因此要解释的主要思想不会遗漏在细节中。这是我们测试任何返回码的第一个程序。在某种程度上,我们必须开发足够的代码才能做到这一点,并且第二个错误检查代码往往不会揭示任何新概念。
文件打开容易失败。该文件可能不存在,可能是因为我们位于错误的文件夹中,或者我们没有足够的访问权限。通常,检查每个系统调用或调用的函数的返回码,但实际上程序员是懒惰的,往往只检查那些可能失败的代码。在此程序中,我们检查两个文件打开调用。
首先,我们必须将文件描述符复制到不会被覆盖的寄存器中,因此将其移至R8。我们使用MOVS指令执行此操作,因此将设置CPSR。
MOVS R8, R0 @ save file descriptor
这意味着我们可以测试它是否为正,如果是,则继续进行下一段代码。
BPL nxtfil @ pos number file opened ok
如果未使用分支,则openFile返回负数。在这里,我们使用writeFile例程将错误消息写入stdout,然后跳转到程序结尾以退出。
MOV R1, #1 @ stdout
LDR R2, =inpErrsz @ Error msg sz
LDR R2, [R2]
writeFile R1, inpErr, R2 @ print the error
B exit
在我们的.data部分中,我们定义了以下错误消息:
inpErr: .asciz "Failed to open input file.\n"
inpErrsz: .word .-inpErr
我们已经看到了.asciz,这是符合标准的。对于writeFile,我们需要字符串的长度来写入控制台。在第1章“入门”中,我们数出了字符串中的字符,并将硬编码的数字放入了代码中。我们也可以在此处执行此操作,但是错误消息开始变得越来越长,数字符数似乎是计算机应该执行的操作。我们可以编写一个类似于C语言库的strlen()函数的例程来计算以NULL结尾的字符串的长度。相反,我们使用了一些GNU汇编器技巧。我们在字符串之后添加.word指令,并使用“ .-inpErr”对其进行初始化。 “. ”是特殊的汇编程序变量,其中包含汇编程序工作时所在的当前地址。因此,字符串后的当前地址减去起始地址就是字符串的长度。
大多数应用程序都包含一个错误模块,因此,如果函数失败,则会调用该错误模块。然后,错误模块负责报告和记录错误。这样,错误报告会变得相当精密,而不会使其他代码与错误处理代码混杂在一起。错误处理代码的另一个问题是它往往未经测试。当错误最终发生时,坏事常常会发生,并且以前未经测试的代码会出现问题。