编写helloworld程序
为了在屏幕上输出指定内容,我们需要调用操作系统内核提供的函数,调用系统调用时,内核将立即暂停程序的执行。然后,它将联系执行硬件上请求的任务 所需的必要驱动程序,然后将控制返回给程序。
注:驱动程序(Drivers)之所以这么称呼是因为内核实际上使用它们来驱动硬件
我们可以通过加载要执行的函数编号(操作代码 OPCODE)并在其余寄存器中填充要传递给系统调用的参数来完成这一切。可以使用 INT 指令请求软件中断,内核接管并调用库中的函数,并采用我们的参数。
例如,当 EAX 设定为 1 将调用 sys_exit 时请求中断,当 EAX 设置为 4 将调用 sys_write 时请求中断。如果函数需要参数 EBX,ECX,EDX 将作为参数传递。
首先,在 .data 部分(SECTION .data)中创建一个变量"msg",并为其分配想要在此例中输出的字符串 “HELLOWORLD!!!”,然后使用 ($ - msg 作为以上字符串的长度)。
在 .text 部分(SECTION .text)中,通过向内核提供全局标签 _start:来表示程序入口点来告诉内核从何处开始执行。
使用系统内核调用 sys_write 将消息输出到控制台窗口。此功能在linux系统中使用操作码为4。该函数还采用3个参数,这些参数在请求将执行任务的软件中断之前,按顺序加载到EDXECX和EBX几个寄存器中。
- EDX:含有字符串的长度信息(单位:Byte)
- ECX:含有字符串的地址(指向字符串的指针)
- EBX:文件输出(此例中是标准输出 STDOUT 到屏幕,也可以输出到文件)
;hello_world.asm
;在屏幕(标准输出)中显示"HelloWorld!!!"
;第一版
SECTION .data
msg db "HelloWorld!!!" , 0ah , 0dh ;0ah(ASCII码:换行符),odh(ASCII码:回车符)
msglen equ ($ - msg)
SECTION .text
global _start
_start:
mov edx,msglen
mov ecx,msg
mov ebx,1d
mov eax,4d
int 80h
码确实跑起来了,但是仍然会报错:
Segmentation fault
这是因为程序还不完整,还缺少一个合适的退出。计算机程序可以被认为是一长串指令,加载到内存中,并分为多个部分(或段)。此常规内存池在所有程序之间共享,可用于存储变量、指令、其他程序或任何真正内容。每个段都指定一个地址,以便以后可以找到存储在该部分中的信息。要执行加载到内存中的程序,我们使用全局标签_start: 告诉操作系统在内存中可以找到和执行我们的程序的位置。然后按照程序逻辑按顺序访问内存,该逻辑确定要访问的下一个地址。操作系统的内核跳转到内存中的该地址并执行它。
请务必告诉操作系统应从何处开始执行,以及应该停止何处。在之前程序中,我们没有告诉内核在哪里停止执行。因此,在调用sys_write之后,程序继续按顺序执行内存中的下一个地址,我们不知道内核试图执行什么,但它导致其终止了我们的进程, 留下了"Segmentation faul"的错误消息。在所有程序的末尾调用 sys_exit 将意味着内核确切知道何时终止进程并将内存返回到常规池,从而避免错误。
在调用 sys_exit 时,需要运用到eax与ebx寄存器:
- EBX:包含一个程序正常退出时返回的状态码,一般使用 “0” ,此处代表没有错误出现
- EAX:置1,来代表调用的是 sys_exit 函数
- 最后使用 int 80h来完成调用
;hello_world.asm
;在屏幕(标准输出)中显示"HelloWorld!!!"
;第二版
SECTION .data
msg db "HelloWorld!!!" , 0ah , 0dh ;0ah(ASCII码:换行符),odh(ASCII码:回车符)
msglen equ ($ - msg)
SECTION .text
global _start
_start:
mov edx,msglen
mov ecx,msg
mov ebx,1d
mov eax,4d
int 80h ;调用linux系统中断,在屏幕上输出字符串
mov ebx,0d ;表示正常退出
mov eax,1d
int 80h ;调用 SYS_EXIT ,正常退出
计算字符串的长度
当我们不清楚到底这个字符串的长度是多少的时候,就需要一种方法来在程序运行时自动确定它的长度
为了计算字符串的长度,我们将使用一种称为指针算术的技术。两个寄存器初始化,指向内存中的同一地址。一个寄存器(在本例中为 EAX)将向前递增一个字节,直到我们到达字符串的末尾。然后将从 EAX 中减去原始指针。这实际上类似于两个数组之间的减法,结果生成两个地址之间的元素数。然后,此结果将传递给sys_write,以替换我们的硬编码计数
CMP 指令将左侧与右侧进行比较,并设置用于程序流的许多标志。我们要检查的标志是 ZF (zero flag) ,零标志位。当 EAX 指向的字节等于零时,将设置 ZF 标志。然后,我们使用 JZ 指令跳转到程序中标记为"已完成"的点(如果设置了 ZF 标志)。这是要从下一个char循环中断,并继续执行程序的其余部分
;hello_len.asm
;自动字符串长度
;使用 nasm -f elf hello_len.asm 汇编
;使用 ld -m elf_i386 hello_len.o -o hello_len 链接
;使用 ./hello_len 运行
SECTION .data
msg db "hhhhhhhhhhhhhhhhhhhelloworld!!!!!!!!", 0ah, 0dh, 0h ;字符串可以修改,同时确保程序的正确性(字符串以 ‘0’ 结尾)
SECTION .text
global _start
_start:
mov eax,msg ;将msg的初地址复制到eax
mov ebx,eax ;将msg的初地址复制到ebx
nextchar:
cmp byte [eax], 0 ;比较eax现在所指向的字符 与 零,(零是字符串的结尾)
;如 cmp oprd1,oprd2
;为第一个数减去第二个数
;但是不影响两个操作数的值
;它影响flag的 CF、ZF、OF、AF、PF 标志位
;若ZF == 1,则两个数相等
;当无符号时
;若CF==1,则出现了借位,即oprd1 < oprd2
;若CF==0,且ZF!=1,则oprd1 > oprd2
jz finished
inc eax
jmp nextchar
finished:
sub eax,ebx ;得到字符串长度,并且保存在eax中
mov edx,eax
mov ecx,msg
mov ebx,1
mov eax,4
int 80h
mov ebx,0
mov eax,1
int 80h
子程序(subroutines)
子程序(又称子例程)相当于函数。它们是可重用的代码段,程序可以调用它们来执行各种可重复的任务。子例程使用标签声明,就像我们以前使用过的标签一样(例如,_start:)但是,我们不使用JMP指令来调用它们 —— 而是使用新的指令CALL。在运行函数后,我们也不会使用 JMP 指令返回到我们的程序。为了从子程序返回到我们的程序,我们改用指令RET。
函数需要使用到的任何寄存器都应该将其当前的值存放到堆栈上,我们使用 PUSH 指令对其进行安全保存。然后,在函数完成执行之后,可以使用 POP 指令还原这些寄存器的原始值。这意味着寄存器中的任何值在调用函数之前与之后都是相同的。如果我们在子程序中处理这一点,我们可以调用函数,而不必担心它们对我们的寄存器进行了哪些更改。
CALL 和 RET 指令也使用堆栈。调用函数时,从调用它时 的地址将被 PUSH 到堆栈上。然后,RET 会弹出此地址,程序将跳回代码中的位置。
;hello.asm
;子例程版本
;使用 nasm -f elf hello.asm 汇编
;使用 ld -m elf_i386 hello.o -o hello 链接
;使用 ./hello 运行
SECTION .data
msg db "hello, ASSWECAN !!!", 0ah, 0dh, 0
SECTION .text
global _start
_start:
mov eax, msg ;把 msg 的初始地址存入eax寄存器
call strlen ;调用函数 strlen 来计算字符串长度
mov edx, eax ;strlen 函数将结果存放在了eax寄存器中
mov ecx, msg ;接下来的与上个程序相似
mov ebx, 1
mov eax, 4
int 80h
mov ebx, 0
mov eax, 1
int 80h
strlen: ;这是我们编写的第一个函数声明
push ebx ;我们将ebx的数据保存到堆栈中,这样它就不会被函数改变
mov ebx, eax;与上一个程序相似
nextchar:
cmp byte [eax], 0
jz finished
inc eax
jmp nextchar
finished:
sub eax, ebx
pop ebx ;将堆栈中保存的ebx值返回到ebx寄存器中
ret ;返回调用函数的地方
外部包含文件
外部包含文件允许我们移动代码,并将其放入单独的文件中。此项技术可用于编写整洁并且易于维护的程序。可重用的代码 可以编写为子例程,并存储在称为库的单独文件中。当你需要调用它们时,可以将该文件包含在程序中,并使用该文件。
functions.asm
;----------------------------------
;int slen(char*)
;计算字符串长度
slen:
push ebx
mov ebx, eax
nextchar:
cmp byte [eax], 0
jz finished
inc eax
jmp nextchar
finished:
sub eax, ebx
pop ebx
ret
;----------------------------------
;void sprint(char*)
;打印字符串
sprint:
push edx
push ecx
push ebx
push eax
call slen
mov edx, eax
pop eax
mov ecx, eax
mov ebx, 1
mov eax, 4
int 80h
pop ebx
pop ecx
pop edx
ret
;----------------------------------
;void exit()
;退出程序
quit:
mov ebx, 0
mov eax, 1
int 80h
ret
test.asm
;test.asm
%include 'functions.asm' ;引用我们的 functions.asm
SECTION .data
msg1 db "helloworld!!!", 0ah, 0dh
msg2 db "This is how we recycle in NASM", 0ah, 0dh
SECTION .text
global _start
_start:
mov eax, msg1
call sprint
mov eax, msg2
call sprint
call quit
NULL终止字节
外部包含程序中的程序是存在问题的:第二条消息输出了两次
实际发生的事情是,我们没有适当地终止我们的字符串。在程序集中,变量是一个接一个地存储在内存中,因此msg1变量的最后一个字节就在msg2变量的第一个字节旁边。我们知道我们的slen函数正在寻找一个零字节,所以除非我们的msg2变量以零字节开头,否则它一直在计数,就好像它是同一个字符串一样(就程序集而言,它们是同一个字符串)。因此,我们需要在字符串之后放置一个零字节或者说0h,以便程序集知道在哪里停止计数。
;hello3.asm
%include 'functions.asm'
SECTION .data
msg1 db "hello !!!!", 0ah, 0dh, 0h
msg2 db "this is invalid!!!", 0ah, 0dh, 0h
SECTION .text
global _start:
_start:
mov eax, msg1
call sprint
mov eax, msg2
call sprint
call quit
换行
换行操作对于控制台程序(如我们的"helloworld"程序)而言,至关重要。一旦我们开始构建需要用户输入的程序,它们就变得更加重要。但换行操作有时会让我们头疼。有时,我们需要将它们包含在字符串中,有时则希望删除它们。如果我们通过在声明的消息文本后添加 0Ah 来继续在变量中硬编码它们,那将成为一个问题。如果代码中有一个位置,我们不想打印出该变量的换行符,我们需要编写一些额外的逻辑,在运行时从字符串中删除它。
如果我们编写一个函数来打印消息,然后打印换行符,则可以提高程序的可维护性。这样,当我们需要换行时,我们可以调用此函数,而不需要时调用之前 sprint 函数。
对sys_write的调用需要将指向要打印的字符串的指针传递给它,这样我们也不能只是将换行符(0Ah)传递给打印函数。我们也不想创建另一个变量只是为了保留换行符,所以我们将改为使用堆栈。
slen: ;修改后的functions.asm
push ebx
mov ebx, eax
nextchar:
cmp byte [eax], 0
jz finished
inc eax
jmp nextchar
finished:
sub eax, ebx
pop ebx
ret
sprint:
push edx
push ecx
push ebx
push eax
call slen
mov edx, eax
pop eax
mov ecx, eax
mov ebx, 1
mov eax, 4
int 80h
pop ebx
pop ecx
pop edx
ret
sprintLF: ;新增的函数,实现自动换行
call sprint
push eax
mov eax, 0ah
push eax
mov eax, esp
call sprint
pop eax
pop eax
ret
quit:
mov ebx, 0
mov eax, 1
int 80h
ret
;test.asm
%include 'functions.asm'
SECTION .data
msg1 db 'hello !!!!!', 0h
msg2 db 'asswecan', 0h
SECTION .text
global _start
_start:
mov eax, msg1
call sprintLF
mov eax, msg2
call sprintLF
call quit
用户输入
关于 SECTION .bss 的简介:
到目前为止,我们已经使用了.text和.data,现在我们即将引入.bss了。BSS 代表着Block Started by Symbol。它是我们程序中的一块区域,用于为未初始化的变量在内存中保留空间。我们将使用它在内存中保留一些空间用来保存我们的用户输入,我们不必知道需要存储多少字节。
使用如下:
SECTION .bss
variableName1: RESB 1 ; 为一个字节保留(reserve)的空间
variableName2: RESW 1 ; 为一个字保留(reserve)的空间
variableName3: RESD 1 ; 为一个双字保留(reserve)的空间
variableName4: RESQ 1 ; 为一个双精度浮点值保留(reserve)的空间
variableName5: REST 1 ; 为一个拓展精度浮点值保留(reserve)的空间
使用系统调用sys_read接收和处理来自用户的输入。它在Linux系统调用表中为OPCODE 3。就像sys_write一样,此函数也采用3个参数,这些参数将在请求将调用该函数的软件中断之前加载到EDX、ECX和EBX中。
- EDX为缓冲区大小(以字节为单位)
- ECX为在.bss部分中创建的变量的地址(变量名)
- EBX为我们想要写入的文件,在这种情况下为 STDIN(标准输入,键盘)
与往常一样,传递的参数的数据类型和含义可以在函数的定义中找到。
;input.asm
%include 'functions.asm'
SECTION .bss
name RESB 255
SECTION .data
msg1 db "hello,what'syourname:", 0h
msg2 db "hello, ", 0h
msg3 db "hello!!", 0h
msg4 db "areyouok??", 0h
SECTION .text
global _start
_start:
mov eax, msg1
call sprint
mov edx, 255
mov ecx, name
mov ebx, 0
mov eax, 3
int 80h
mov eax, msg2
call sprint
mov eax, name
call sprint
mov eax, msg3
call sprintLF
mov eax, msg4
call sprintLF
call quit