编写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
最后修改:2023 年 06 月 05 日
如果觉得我的文章对你有用,请随意赞赏