博客地址:NASM Tutorial 1
另一个翻译教程:NASM Tutorial 2
文中的汇编程序均可运行于 Linux 上。
本文只是翻译了英文教程:NASM Assembly Language Tutorials - asmtutor.com
NASM 其它参考资料:
汇编语言是最基本的。程序员在实际硬件之上拥有的唯一接口是内核本身。为了在汇编中构建有用的程序,我们需要使用内核提供的 Linux 系统调用。这些系统调用是操作系统中内置的库,用于提供一些功能,比如从键盘读取输入和将输出写入屏幕等等。
当你调用系统调用时,内核将立即暂停你程序的执行。然后,它将通知必要的驱动程序,以在硬件上执行您请求的任务,然后将控制权返回给你的程序。
Note: 驱动程序 (Drivers) 之所以称为“驱动程序”,是因为内核确实是使用它们来驱动硬件。
我们可以通过在 EAX
中加载我们想要执行的函数号(OPCODE
),并用我们想要传递给 系统调用 的参数填充其它寄存器。用 INT
指令请求软件中断,内核接管并用我们的参数从库中调用函数,从而完成上述系统调用。单击此处查看Linux系统调用表及其相应OPCODES的示例
例如,当 EAX=1
时请求中断将调用 sys_exit
,而当 EAX=4
时请求中断则将调用 sys_write
。如果有需要,EBX
、ECX
和 EDX
可以传递参数。
首先,我们在 .data
节中创建一个变量 msg
,并将要输出的字符串分配给它,在本例中为 “Hello, world!
” 。在 .text
节,我们通过向内核提供一个全局标签 _start
来指示程序入口点,从而告诉内核从哪里开始执行。
我们使用系统调用 sys_write
将信息输出到控制台窗口。此函数在 Linux 系统调用表中指定为 OPCODE 4
。在请求执行任务的软件中断之前,该函数还接受 3 个参数,这些参数依次加载到 EDX
、ECX
和 EBX
中。
参数传递如下:
EDX
置为字符串的长度(以字节为单位)ECX
置为 .data
节中创建的变量的地址。EBX
置为目的文件(在本例中为标准输出 STDOUT
)。使用以下命令编译、链接和运行程序。
; Hello, world!
; 文件名:helloworld.asm
; 编译:nasm -f elf helloworld.asm
; 链接(64位系统需要 elf_i386 选项):ld -m elf_i386 helloworld.o -o helloworld
; 运行:./helloworld
SECTION .data
msg db 'Hello World!', 0Ah ; 初始化变量msg
SECTION .text
global _start
_start:
mov edx, 13 ; 待写入的字节数(每个字母一个字节,再加上换行符0Ah)
mov ecx, msg ; 将msg的内存地址移入ecx
mov ebx, 1 ; 表示写入到标准输出STDOUT
mov eax, 4 ; 调用SYS_WRITE(OPCODE是4)
int 80h
~$ nasm -f elf helloworld.asm
~$ ld -m elf_i386 helloworld.o -o helloworld
~$ ./helloworld
Hello World!
Segmentation fault (core dumped)
Error: Segmentation fault (core dumped)
在 Lesson 1 中成功学习了如何执行系统调用之后,现在需要学习内核中最重要的系统调用之一 sys_exit
。
我们的 “Hello, world!
” 程序运行时出现了 段错误 Segmentation fault
?计算机程序可以被认为是加载到内存中的一长条指令,并被分成多个节 sections
(或多个段 segments
)。通用内存池在所有程序之间共享,可以用来存储变量、指令、其他程序等等。每个段都有一个地址,以便以后可以找到存储在该段中的信息。
要执行加载到内存中的程序,我们使用全局标签 _start
来告诉操作系统在内存中的什么位置可以找到并执行我们的程序。然后,按照程序逻辑顺序访问内存,程序逻辑决定要访问的下一个地址。内核跳转到内存中的那个地址并执行它。
告诉操作系统应该在哪里开始执行,在哪里停止,这一点很重要。在 Lession 1中,我们没有告诉内核在哪里停止执行。因此,在我们调用 sys_write
之后,程序继续顺序执行内存中的下一个地址,这可能是任何地址。我们不知道内核试图执行什么,但最终其阻塞,终止进程并报错 “Segmentation fault
” 。在所有程序结束时调用 sys_exit
将意味着内核确切知道何时终止进程并将内存归还到通用池,从而避免错误。
sys_exit
有一个简单的函数定义。在 Linux 系统调用表中,它被分配为OPCODE 1
,并通过 EBX
传递单个参数。
为了执行此函数,我们需要做:
EBX
置为 0
,表示 zero errors
,即没有错误EAX
置为 1
,来调用 sys_exit
INT 80h
请求中断。然后,我们再次编译、链接并运行它。
; Proper program exit
; 文件名:helloworld.asm
; 编译:nasm -f elf helloworld.asm
; 链接(64位系统需要 elf_i386 选项):ld -m elf_i386 helloworld.o -o helloworld
; 运行:./helloworld
SECTION .data
msg db 'Hello World!', 0Ah ; 初始化变量msg
SECTION .text
global _start
_start:
mov edx, 13 ; 待写入的字节数(每个字母一个字节,再加上换行符0Ah)
mov ecx, msg ; 将msg的内存地址移入ecx
mov ebx, 1 ; 表示写入到标准输出STDOUT
mov eax, 4 ; 调用SYS_WRITE(操作码是4)
int 80h
mov ebx, 0 ; exit时返回状态0 - 'No Errors'
mov eax, 1 ; 调用SYS_EXIT(OPCODE是1)
int 80h
~$ nasm -f elf helloworld.asm
~$ ld -m elf_i386 helloworld.o -o helloworld
~$ ./helloworld
Hello World!
为什么需要计算字符串的长度?
sys_write
要求我们向它传递一个指向待输出字符串的指针,以及字符串的字节长度。如果修改了字符串 msg
,我们还必须传递给 sys_write
新的字节长度,否则它将无法正确打印。
例如,在 Lesson 2 的程序中,修改字符串 msg
为 “Hello, brave new world!
“,然后编译、链接并运行新程序。其输出将是 “Hello, brave ”(前13个字符),因为我们仍然只向 sys_write
传递13个字节作为其长度。特别是当我们想打印出用户输入时,计算字符串的长度是必要的。我们在编译程序时不知道数据的长度,因此需要一种方法来计算运行时的长度,以便成功地将其打印出来。
为了计算字符串的长度,我们将使用一种称为指针算法的技术。两个寄存器被初始化,指向内存中的同一地址。对于输出字符串中的每个字符,一个寄存器(在本例中为 EAX
)将向前递增一个字节,直到到达字符串的末尾。然后将从 EAX
中减去原始指针。这实际上类似于两个数组之间的减法,结果产生两个地址之间的元素数。然后将结果传递给 sys_write
,替换硬编码计数。
CMP
指令将左操作数与左操作数进行比较,以对标志寄存器产生影响,其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。在本例中,我们检查零标志位 ZF
(Zero Flag) 。当 EAX
指向的字节等于 \0
(0 是字符串的末尾)时,设置 ZF
标志位为 1
。然后,如果 ZF
标志位 为 1
,我们使用 JZ
指令跳到程序中标记为 finished
的地方。这是为了调出 nextchar
循环,继续执行程序的其余部分。
算法大致是:
EBX
和 EAX
先同时赋值为 msg
的地址;EAX
递增(地址+1),比较 [eax]=='\0'
,如果不是 0 就继续递增 EAX
,否则退出EAX-EBX
,这样就是字符串的长度了,其保存在 EAX
中; Calculate string length
; 文件名:helloworld-len.asm
; 编译:nasm -f elf helloworld-len.asm
; 链接(64位系统需要 elf_i386 选项):ld -m elf_i386 helloworld-len.o -o helloworld-len
; 运行:./helloworld-len
SECTION .data
msg db 'Hello, brave new world!', 0Ah ; 现在可以在不改变其它地方的情况下修改msg
SECTION .text
global _start
_start:
mov ebx, msg ; 将msg的内存地址移入EBX
mov eax, ebx ; 地址也移入EAX(现在二者指向内存中同一处)
nextchar:
cmp byte [eax], 0 ; 比较当前EAX地址处的内容是否为字符串结尾'\0'
jz finished ; ZF为1,跳出循环到finished
inc eax ; ZF不为1,EAX中的地址递增
jmp nextchar ; 继续循环
finished:
sub eax, ebx ; EBX - EAX,长度保存在EAX中
; 两个寄存器开始时指向同一地址 (line 15)
; 但EAX与msg中的字符对应地递增
; 当同一类型的两个内存地址相减
; 结果就是地址间的段数量,本例中就是字节数
mov edx, eax ; EAX现在便是字符串的长度
mov ecx, msg ; 将msg的内存地址移入ecx
mov ebx, 1 ; 表示写入到标准输出STDOUT
mov eax, 4 ; 调用SYS_WRITE(操作码是4)
int 80h
mov ebx, 0 ; exit时返回状态0 - 'No Errors'
mov eax, 1 ; 调用SYS_EXIT(OPCODE是1)
int 80h
~$ nasm -f elf helloworld-len.asm
~$ ld -m elf_i386 helloworld-len.o -o helloworld-len
~$ ./helloworld-len
Hello, brave new world!
函数 / 子程序 (functions/subroutines)是可重用的代码片段,程序可以调用它们来执行各种可重复的任务。函数是使用 标签 声明的,就像我们之前使用的那样(例如 _start:
),但是我们不使用 JMP
指令来访问它们,而是使用新的指令 CALL
。在运行函数后,也不会使用 JMP
指令返回到原程序。要从函数返回到原程序,使用指令 RET
。
编写函数的好处就是可以重用它。如果我们想从代码中的任何地方使用函数,可能必须编写一些逻辑代码来确定我们从代码中跳到了哪里,并且应该跳回到哪里。这会使我们的代码中充斥着不需要的标签。但是,如果我们使用 CALL
和 RET
,程序会使用栈(stack)为我们处理这个问题。
栈(stack)是一种特殊类型的内存。它与我们以前使用过的内存类型相同,但在程序使用它的方式上很特殊。栈称为后进先出内存(Last In First Out,LIFO)。可以把栈想象成厨房里的一栈盘子,放在栈上的最后一个盘子也是你下次使用盘子时将从栈上取下的第一个盘子。
汇编程序中栈的不是存储盘子,而是存储值。你可以在堆栈上存储很多东西,比如变量、地址或其他程序。当调用函数时,我们需要使用栈来临时存储函数返回后要恢复的值。
函数需要使用的任何寄存器都应该将其当前值放入栈中,以便使用 PUSH
指令安全保存。然后,在函数完成其逻辑之后,这些寄存器可以使用 POP
指令恢复其原始值。这样便可保证在调用函数前后,寄存器中的任何值都没有变化。如果我们在函数中处理好这一点,我们就可以随意调用函数,而不用担心它们对寄存器做了什么更改。
CALL
和 RET
指令也使用栈。当 CALL
一个函数时,调用地址被push到栈上。函数结束时,RET
将这个地址从栈中pop,以跳回到调用位置。
; Functions
; 文件名:helloworld-len.asm
; 编译:nasm -f elf helloworld-len.asm
; 链接(64位系统需要 elf_i386 选项):ld -m elf_i386 helloworld-len.o -o helloworld-len
; 运行:./helloworld-len
SECTION .data
msg db 'Hello, brave new world!', 0Ah
SECTION .text
global _start
_start:
mov eax, msg ; 将msg的内存地址移入EAX
call strlen ; 调用函数strlen计算字符串长度
mov edx, eax ; 函数将结果放在了EAX
mov ecx, msg ; 将msg的内存地址移入ecx
mov ebx, 1 ; 表示写入到标准输出STDOUT
mov eax, 4 ; 调用SYS_WRITE(操作码是4)
int 80h
mov ebx, 0 ; exit时返回状态0 - 'No Errors'
mov eax, 1 ; 调用SYS_EXIT(OPCODE是1)
int 80h
strlen: ; strlen函数声明,返回值在EAX中
push ebx ; 将EBX中的值保存于栈上,因为strlen会使用该寄存器
mov ebx, eax ; 将EAX中msg的地址移EBX(现在二者指向内存中同一处)
nextchar: ; 与 Lesson3 中相同
cmp byte [eax], 0 ; 比较当前EAX地址处的内容是否为字符串结尾'\0'
jz finished ; ZF为1,跳出循环到finished
inc eax ; ZF不为1,EAX中的地址递增
jmp nextchar ; 继续循环
finished:
sub eax, ebx ; EBX - EAX,长度保存在EAX中
pop ebx ; 将栈上之前保存的值pop回EBX
ret ; 返回函数调用处
~$ nasm -f elf helloworld-len.asm
~$ ld -m elf_i386 helloworld-len.o -o helloworld-len
~$ ./helloworld-len
Hello, brave new world!
外部包含文件 (External include files) 允许我们将代码从程序中移出并放入单独的文件中。这种技术对于编写清晰、易于维护的程序很有用。可重用的代码可以作为子程序编写,并存储在称为库的单独文件中。当你需要一段代码逻辑时,可以将该文件包含 (include) 在程序中,将其视作同一文件的一部分。
本例中,我们将把字符串长度计算子程序单独定义在一个外部文件中。同时,字符串打印函数和程序退出函数也定义在这个外部文件中。这样程序将更加清晰可读。
这样,我们可以声明再声明一个变量,调用 print
函数两次,来演示如何重用代码。
; 文件名:functions.asm
; 定义了三个函数:
; 1. strlen(String msg): 计算字符串长度
; 2. sprint(String msg): 打印字符串
; 3. quit(): 退出程序
;------------------------------------------
; int strlen(String message)
; String length calculation function
strlen: ; 返回值保存在EAX中
push ebx ; 将EBX中的值保存于栈上,因为strlen会使用该寄存器
mov ebx, eax ; 将EAX中msg的地址移EBX(现在二者指向内存中同一处)
nextchar: ; 与 Lesson3 中相同
cmp byte [eax], 0 ; 比较当前EAX地址处的内容是否为字符串结尾'\0'
jz finished ; ZF为1,跳出循环到finished
inc eax ; ZF不为1,EAX中的地址递增
jmp nextchar ; 继续循环
finished:
sub eax, ebx ; EBX - EAX,长度保存在EAX中
pop ebx ; 将栈上之前保存的值pop回EBX
ret ; 返回函数调用处
;------------------------------------------
; void sprint(String message)
; String printing function
sprint:
push edx ; 将EDX中的值保存于栈上
push ecx ; 将ECX中的值保存于栈上
push ebx ; 将EBX中的值保存于栈上
push eax ; 将EAX中的值保存于栈上,即参数string
call strlen ; 计算EAX中字符串长度,保存在EAX中
mov edx, eax ; 将长度移入到EDX
pop eax ; 恢复EAX值,即参数string
mov ecx, eax ; 将待打印string移入ECX
mov ebx, 1 ; 表示写入到标准输出STDOUT
mov eax, 4 ; 调用SYS_WRITE(操作码是4)
int 80h
pop ebx ; 恢复原来EBX中的值
pop ecx ; 恢复原来ECX中的值
pop edx ; 恢复原来EDX中的值
ret
;------------------------------------------
; void quit()
; Exit program and restore resources
quit:
mov ebx, 0 ; exit时返回状态0 - 'No Errors'
mov eax, 1 ; 调用SYS_EXIT(OPCODE是1)
int 80h
ret
; External file include
; 文件名:helloworld-inc.asm
; 编译:nasm -f elf helloworld-inc.asm
; 链接(64位系统需要 elf_i386 选项):ld -m elf_i386 helloworld-inc.o -o helloworld-inc
; 运行:./helloworld-inc
%include 'functions.asm' ; 引入外部文件functions.asm
SECTION .data
msg1 db 'Hello, brave new world!', 0Ah ; msg1
msg2 db 'This is how we recycle in NASM.', 0Ah ; msg2
SECTION .text
global _start
_start:
mov eax, msg1 ; 将第一个字符串的地址移入EAX
call sprint ; 调用字符串打印函数
mov eax, msg2 ; 将第二个字符串的地址移入EAX
call sprint ; 调用字符串打印函数
call quit ; 调用程序退出函数
~$ nasm -f elf helloworld-inc.asm
~$ ld -m elf_i386 helloworld-inc.o -o helloworld-inc
~$ ./helloworld-inc
Hello, brave new world!
This is how we recycle in NASM.
This is how we recycle in NASM.
Error: 第二个字符串打印了 2 次,这将在下一 Lesson 中修复。
为什么 Lesson 5 中的第二个字符串打印了两次,而我们只在 msg2
上调用了一次 sprint
函数?如果将第二次的 sprint
调用注释掉,结果如下,我们发现在 Lesson 5 中第二次的 sprint
实际上只打印了一次,但 第一次的 sprint
将两个字符串都打印了。
Hello, brave new world!
This is how we recycle in NASM.
But why?
那是因为我们没有正确终止字符串。在汇编中,变量一个接一个地存储在内存中,因此 msg1
的最后一个字节紧邻 msg2
的第一个字节。我们知道字符串长度计算是在寻找零字节 \0
,因此除非 msg2
变量以零字节开头,否则它会一直计数,就像它是同一个字符串一样(对汇编来说,它是相同的字符串)。所以,我们需要在字符串后面放一个零字节 0h
,让程序集知道在哪里停止计数。
**Note: **在程序中,0h
表示一个空字节(null
),字符串后的空字节告诉汇编程序它在内存中的结束位置。
; NULL terminating bytes
; 文件名:helloworld-inc.asm
; 编译:nasm -f elf helloworld-inc.asm
; 链接(64位系统需要 elf_i386 选项):ld -m elf_i386 helloworld-inc.o -o helloworld-inc
; 运行:./helloworld-inc
%include 'functions.asm' ; 引入外部文件functions.asm
SECTION .data
msg1 db 'Hello, brave new world!', 0Ah, 0h ; 注意字符串结束符0h
msg2 db 'This is how we recycle in NASM.', 0Ah, 0h ; 注意字符串结束符0h
SECTION .text
global _start
_start:
mov eax, msg1 ; 将第一个字符串的地址移入EAX
call sprint ; 调用字符串打印函数
mov eax, msg2 ; 将第二个字符串的地址移入EAX
call sprint ; 调用字符串打印函数
call quit ; 调用程序退出函数
~$ nasm -f elf helloworld-inc.asm
~$ ld -m elf_i386 helloworld-inc.o -o helloworld-inc
~$ ./helloworld-inc
Hello, brave new world!
This is how we recycle in NASM.
换行符(Linefeeds)对于需要用户输入的程序很重要。但 换行符 维护起来可能很麻烦。有时你想把它们加在字符串中,有时你想删除它们。如果声明字符串变量时在其后添加了 0Ah
,但代码中有一处地方我们不想打印出该变量的 换行符,那么需要编写一些额外的逻辑代码,在运行时将其从字符串中删除。
如果编写一个新的函数 sprintLF
来打印字符串+换行,那么对于程序的可维护性来说会更好。这样,就可以在需要打印换行符时调用这个新函数 sprintLF
,而在不需要时调用 sprint
。
sys_write
的调用要求传递一个指向待打印字符串地址的指针,因此我们不能只向打印函数传递换行符 0Ah
。但我们也不想仅仅为了保存换行符而创建另一个变量,于是我们使用栈(stack)。
将换行字符移动到 EAX
中,然后将 EAX
push 到栈上,并获取 Extended Stack Pointer 指向的地址。ESP
是一个寄存器,当将元素推送到堆栈中时,ESP
将递减,以指向最后一个元素在内存中的地址,因此可以使用它直接从栈中访问该元素。所以 sys_write
能够通过 ESP
指 0Ah
在内存中的地址打印换行符。
; 文件名:functions.asm
; 定义了四个函数:
; 1. int strlen(String msg): 计算字符串长度
; 2. void sprint(String msg): 打印字符串
; 3. void sprintLF(String message): 打印字符串+换行符
; 4. void quit(): 退出程序
;------------------------------------------
; int strlen(String message) 返回值保存在EAX中
; String length calculation function
strlen: ; 返回值保存在EAX中
push ebx ; 将EBX中的值保存于栈上,因为strlen会使用该寄存器
mov ebx, eax ; 将EAX中msg的地址移EBX(现在二者指向内存中同一处)
nextchar: ; 与 Lesson3 中相同
cmp byte [eax], 0 ; 比较当前EAX地址处的内容是否为字符串结尾'\0'
jz finished ; ZF为1,跳出循环到finished
inc eax ; ZF不为1,EAX中的地址递增
jmp nextchar ; 继续循环
finished:
sub eax, ebx ; EBX - EAX,长度保存在EAX中
pop ebx ; 将栈上之前保存的值pop回EBX
ret ; 返回函数调用处
;------------------------------------------
; void sprint(String message)
; String printing function
sprint:
push edx ; 将EDX中的值保存于栈上
push ecx ; 将ECX中的值保存于栈上
push ebx ; 将EBX中的值保存于栈上
push eax ; 将EAX中的值保存于栈上,即参数string
call strlen ; 计算EAX中字符串长度,保存在EAX中
mov edx, eax ; 将长度移入到EDX
pop eax ; 恢复EAX值,即参数string
mov ecx, eax ; 将待打印string移入ECX
mov ebx, 1 ; 表示写入到标准输出STDOUT
mov eax, 4 ; 调用SYS_WRITE(操作码是4)
int 80h
pop ebx ; 恢复原来EBX中的值
pop ecx ; 恢复原来ECX中的值
pop edx ; 恢复原来EDX中的值
ret
;------------------------------------------
; void sprintLF(String message)
; String printing with line feed function
sprintLF:
call sprint
push eax ; 将EAX中的值保存于栈上,即参数string
mov eax, 0Ah ; 将换行符0Ah移入EAX
push eax ; 将换行符0Ah入栈,这样可以获取其地址
mov eax, esp ; 将当前栈指针ESP中的地址(指向0Ah)移入EAX
call sprint ; 调用sprint打印换行符
pop eax ; 换行符退栈
pop eax ; 恢复调用该函数前EAX中的值
ret ; 返回调用处
;------------------------------------------
; void quit()
; Exit program and restore resources
quit:
mov ebx, 0 ; exit时返回状态0 - 'No Errors'
mov eax, 1 ; 调用SYS_EXIT(OPCODE是1)
int 80h
ret
; Print with line feed
; 文件名:helloworld-inc.asm
; 编译:nasm -f elf helloworld-inc.asm
; 链接(64位系统需要 elf_i386 选项):ld -m elf_i386 helloworld-inc.o -o helloworld-inc
; 运行:./helloworld-inc
%include 'functions.asm' ; 引入外部文件functions.asm
SECTION .data
msg1 db 'Hello, brave new world!', 0h ; 注意移除了换行符0Ah
msg2 db 'This is how we recycle in NASM.', 0h ; 注意移除了换行符0Ah
SECTION .text
global _start
_start:
mov eax, msg1 ; 将第一个字符串的地址移入EAX
call sprintLF ; 调用新的字符串打印函数,自动打印换行符
mov eax, msg2 ; 将第二个字符串的地址移入EAX
call sprintLF ; 调用新的字符串打印函数,自动打印换行符
call quit ; 调用程序退出函数
~$ nasm -f elf helloworld-lf.asm
~$ ld -m elf_i386 helloworld-lf.o -o helloworld-lf
~$ ./helloworld-lf
Hello, brave new world!
This is how we recycle in NASM.
从命令行向程序传递参数就像在 NASM 中将它们从栈中弹出一样简单。当我们运行程序时,任何传递的参数都以相反的顺序加载到栈中,然后将程序名加载到栈,最后将参数总数加载到栈。NASM 编译的程序的最后两个栈元素始终是程序名和传递的参数总数。
因此,要使用参数,首先要从栈中弹出参数的数量,然后遍历参数,进行相关操作如打印出来。
Note: 我们使用 ECX
寄存器作为循环的计数器(Counter)。虽然它是一个通用寄存器,但其设计初衷是用作计数器。
; Passing arguments from the command line
; 文件名:helloworld-args.asm
; 编译:nasm -f elf helloworld-args.asm
; 链接(64位系统需要 elf_i386 选项):ld -m elf_i386 helloworld-args.o -o helloworld-args
; 运行:./helloworld-args
%include 'functions.asm'
SECTION .text
global _start
_start:
pop ecx ; 栈上的一个元素是参数总数
nextArg:
cmp ecx, 0h ; 判断是否还要参数剩余
jz noMoreArgs ; 如果ZF为1,说明没有参数,跳出程序
pop eax ; 下一个函数参数出栈
call sprintLF ; 调用字符串+换行符打印函数
dec ecx ; ECX中的值(剩余参数数量)递减
jmp nextArg ; 继续循环
noMoreArgs:
call quit
~$ nasm -f elf helloworld-args.asm
~$ ld -m elf_i386 helloworld-args.o -o helloworld-args
~$ ./helloworld-args "This is one argument" "This is another" 101
./helloworld-args
This is one argument
This is another
101
到目前为止,我们已经使用了.text
和 .data
节,现在是时候介绍一下 .bss
节了。BSS(Block Started by Symbol)用于在内存中为未初始化的变量保留空间。我们将使用它在内存中预留一些空间来保存用户输入,因为我们不知道需要存储多少字节。
声明变量的语法如下:
SECTION .bss
variableName1: RESB 1 ; reserve space for 1 byte
variableName2: RESW 1 ; reserve space for 1 word
variableName3: RESD 1 ; reserve space for 1 double word
variableName4: RESQ 1 ; reserve space for 1 double precision float (quad word)
variableName5: REST 1 ; reserve space for 1 extended precision float
我们将使用系统调用 sys_read
来接收和处理来自用户的输入。此函数在 Linux 系统调用表中指定为 OPCODE 3
。就像 sys_write
一样,该函数也有3个参数,在请求调用该函数的软件中断之前,这些参数将被加载到 EDX
、ECX
和 EBX
中。
参数传递如下:
EDX
置为最大内存字节数(以字节为单位)。ECX
置为在 .bss
节中创建的变量的地址。EBX
置源文件(在本例中为标准输入 STDIN
)。当 sys_read
检测到换行符时,将返回到程序,用户的输入便存在 ECX
中的内存地址处了。
; Getting input
; 文件名:helloworld-input.asm
; 编译:nasm -f elf helloworld-input.asm
; 链接(64位系统需要 elf_i386 选项):ld -m elf_i386 helloworld-input.o -o helloworld-input
; 运行:./helloworld-input
%include 'functions.asm'
SECTION .data
msg1 db 'Please enter your name: ', 0h ; 用于提示用户输入
msg2 db 'Hello, ', 0h ; 用于与用户打招呼
SECTION .bss
sinput: resb 255 ; 预留255字节空间来保存用户输入
SECTION .text
global _start
_start:
mov eax, msg1 ; 将提示字符串移入EAX
call sprint ; 打印提示字符串
mov edx, 255 ; 要读取的字符串字节数
mov ecx, sinput ; 缓冲变量地址 (buffer)
mov ebx, 0 ; write to the STDIN file
mov eax, 3 ; 调用SYS_READ (OPCODE为3)
int 80h
mov eax, msg2
call sprint ; 打印 'Hello, '
mov eax, sinput ; 将用户输入移入EAX(注意:输入包含换行符)
call sprint ; 调用打印函数
call quit
~$ nasm -f elf helloworld-input.asm
~$ ld -m elf_i386 helloworld-input.o -o helloworld-input
~$ ./helloworld-input
Please enter your name: Cloudy1225
Hello, Cloudy1225
汇编中数字计数并不像想象中的那样简单。首先,我们需要在内存中传递 sys_write
地址,这样我们就不能直接用数字来填充寄存器并调用 print 函数。其次,数字和字符串在汇编中是完全不同的东西。字符串由 ASCII
码表示。ASCII
代表美国信息交换标准码(American Standard Code for Information Interchange)。ASCII
的创建标准化了所有计算机上的字符表示。
记住,不能打印数字,必须打印字符串。我们需要将数字从标准整数转换为 ASCII
码表示。查看 ASCII
码表,可以看到数字 “1” 的字符表示实际上是 ASCII
中的“49”。故将 48 加到数字上,就可以将它们从整数转换为 ASCII
表示。
我们使用 ECX
作为计数器来从 1 数到 10,对每个数 ADD
48,将其从数字转换为 ASCII
表示。然后,将此值入栈,并调用打印函数, ESP
将待打印值的地址传递给我们。一旦数到10,便退出计数循环并调用退出函数。
; Print 1 to 10
; 文件名:helloworld-10.asm
; 编译:nasm -f elf helloworld-10.asm
; 链接(64位系统需要 elf_i386 选项):ld -m elf_i386 helloworld-10.o -o helloworld-10
; 运行:./helloworld-10
%include 'functions.asm'
SECTION .text
global _start
_start:
mov ecx, 0 ; 计数器ECX初始化为0
nextNumber:
inc ecx ; ECX递增
mov eax, ecx ; 将ECX中的值移入EAX
add eax, 48 ; +48来转为ASCII码表示
push eax ; 待打印字符入栈
mov eax, esp ; 获取待打印字符的地址
call sprintLF ; 调用打印函数
pop eax ; 移除不再需要的字符
cmp ecx, 10 ; 是否数到10
jne nextNumber ; 没数到就继续循环
call quit
~$ nasm -f elf helloworld-10.asm
~$ ld -m elf_i386 helloworld-10.o -o helloworld-10
~$ ./helloworld-10
1
2
3
4
5
6
7
8
9
:
Error: 数字 10 变成了冒号 ‘:’ ???
为什么 Lesson 10 中的程序会打印冒号字符而不是数字 10 呢?让我们看看 ASCII
表。我们可以看到冒号的 ASCII
值为58。我们将 48
加到整数中,以将它们转换为ASCII字符串表示形式,因此我们实际上需要传递数字 1 的 ASCII
数值,然后再传递数字 0 的 ASCII
值,而不是传递给 sys_write
值 “58” 以打印10。传递给 sys_ write
“4948” 才是数字 “10” 的正确字符串表示形式。因此,不能简单地将 48 加到数字上进行转换,我们首先必须将它们除以 10,因为每个位值都需要单独进行转换。