当前位置: 首页 > 工具软件 > NASM > 使用案例 >

NASM汇编初探(入门教程)

隗瑞
2023-12-01

Learn Assembly Language


初学汇编,一脸懵逼?!

博客地址:NASM Tutorial 1

另一个翻译教程:NASM Tutorial 2

文中的汇编程序均可运行于 Linux 上。

本文只是翻译了英文教程:NASM Assembly Language Tutorials - asmtutor.com

NASM 其它参考资料:

1. Lesson 1 Hello, world!

1.1 Background

汇编语言是最基本的。程序员在实际硬件之上拥有的唯一接口是内核本身。为了在汇编中构建有用的程序,我们需要使用内核提供的 Linux 系统调用。这些系统调用是操作系统中内置的库,用于提供一些功能,比如从键盘读取输入和将输出写入屏幕等等。

当你调用系统调用时,内核将立即暂停你程序的执行。然后,它将通知必要的驱动程序,以在硬件上执行您请求的任务,然后将控制权返回给你的程序。

Note: 驱动程序 (Drivers) 之所以称为“驱动程序”,是因为内核确实是使用它们来驱动硬件。

我们可以通过在 EAX 中加载我们想要执行的函数号(OPCODE),并用我们想要传递给 系统调用 的参数填充其它寄存器。用 INT 指令请求软件中断,内核接管并用我们的参数从库中调用函数,从而完成上述系统调用。单击此处查看Linux系统调用表及其相应OPCODES的示例

例如,当 EAX=1 时请求中断将调用 sys_exit ,而当 EAX=4 时请求中断则将调用 sys_write 。如果有需要,EBXECXEDX 可以传递参数。


1.2 Writing our program

首先,我们在 .data 节中创建一个变量 msg ,并将要输出的字符串分配给它,在本例中为 “Hello, world!” 。在 .text 节,我们通过向内核提供一个全局标签 _start 来指示程序入口点,从而告诉内核从哪里开始执行。

我们使用系统调用 sys_write 将信息输出到控制台窗口。此函数在 Linux 系统调用表中指定为 OPCODE 4 。在请求执行任务的软件中断之前,该函数还接受 3 个参数,这些参数依次加载到 EDXECXEBX 中。

参数传递如下:

  • 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)


2. Lesson 2 合适的程序退出

2.1 Background

在 Lesson 1 中成功学习了如何执行系统调用之后,现在需要学习内核中最重要的系统调用之一 sys_exit

我们的 “Hello, world!” 程序运行时出现了 段错误 Segmentation fault ?计算机程序可以被认为是加载到内存中的一长条指令,并被分成多个节 sections(或多个段 segments)。通用内存池在所有程序之间共享,可以用来存储变量、指令、其他程序等等。每个段都有一个地址,以便以后可以找到存储在该段中的信息。

要执行加载到内存中的程序,我们使用全局标签 _start 来告诉操作系统在内存中的什么位置可以找到并执行我们的程序。然后,按照程序逻辑顺序访问内存,程序逻辑决定要访问的下一个地址。内核跳转到内存中的那个地址并执行它。

告诉操作系统应该在哪里开始执行,在哪里停止,这一点很重要。在 Lession 1中,我们没有告诉内核在哪里停止执行。因此,在我们调用 sys_write 之后,程序继续顺序执行内存中的下一个地址,这可能是任何地址。我们不知道内核试图执行什么,但最终其阻塞,终止进程并报错 “Segmentation fault” 。在所有程序结束时调用 sys_exit 将意味着内核确切知道何时终止进程并将内存归还到通用池,从而避免错误。


2.2 Writing our program

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!

3. Lesson 3 计算字符串长度

3.1 Background

为什么需要计算字符串的长度?

sys_write 要求我们向它传递一个指向待输出字符串的指针,以及字符串的字节长度。如果修改了字符串 msg,我们还必须传递给 sys_write 新的字节长度,否则它将无法正确打印。

例如,在 Lesson 2 的程序中,修改字符串 msg 为 “Hello, brave new world!“,然后编译、链接并运行新程序。其输出将是 “Hello, brave ”(前13个字符),因为我们仍然只向 sys_write 传递13个字节作为其长度。特别是当我们想打印出用户输入时,计算字符串的长度是必要的。我们在编译程序时不知道数据的长度,因此需要一种方法来计算运行时的长度,以便成功地将其打印出来。


3.2 Writing our program

为了计算字符串的长度,我们将使用一种称为指针算法的技术。两个寄存器被初始化,指向内存中的同一地址。对于输出字符串中的每个字符,一个寄存器(在本例中为 EAX )将向前递增一个字节,直到到达字符串的末尾。然后将从 EAX 中减去原始指针。这实际上类似于两个数组之间的减法,结果产生两个地址之间的元素数。然后将结果传递给 sys_write,替换硬编码计数。

CMP 指令将左操作数与左操作数进行比较,以对标志寄存器产生影响,其他相关指令通过识别这些被影响的标志寄存器位来得知比较结果。在本例中,我们检查零标志位 ZF (Zero Flag) 。当 EAX 指向的字节等于 \0 (0 是字符串的末尾)时,设置 ZF 标志位为 1。然后,如果 ZF 标志位 为 1,我们使用 JZ 指令跳到程序中标记为 finished 的地方。这是为了调出 nextchar 循环,继续执行程序的其余部分。

算法大致是:

  1. EBXEAX 先同时赋值为 msg 的地址;
  2. EAX 递增(地址+1),比较 [eax]=='\0',如果不是 0 就继续递增 EAX,否则退出
  3. 计算 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!

4. Lesson 4 函数

4.1 Introduction to functions

函数 / 子程序 (functions/subroutines)是可重用的代码片段,程序可以调用它们来执行各种可重复的任务。函数是使用 标签 声明的,就像我们之前使用的那样(例如 _start:),但是我们不使用 JMP 指令来访问它们,而是使用新的指令 CALL。在运行函数后,也不会使用 JMP 指令返回到原程序。要从函数返回到原程序,使用指令 RET


4.2 为什么不用JMP

编写函数的好处就是可以重用它。如果我们想从代码中的任何地方使用函数,可能必须编写一些逻辑代码来确定我们从代码中跳到了哪里,并且应该跳回到哪里。这会使我们的代码中充斥着不需要的标签。但是,如果我们使用 CALLRET,程序会使用栈(stack)为我们处理这个问题。


4.3 栈

栈(stack)是一种特殊类型的内存。它与我们以前使用过的内存类型相同,但在程序使用它的方式上很特殊。栈称为后进先出内存(Last In First Out,LIFO)。可以把栈想象成厨房里的一栈盘子,放在栈上的最后一个盘子也是你下次使用盘子时将从栈上取下的第一个盘子。

汇编程序中栈的不是存储盘子,而是存储值。你可以在堆栈上存储很多东西,比如变量、地址或其他程序。当调用函数时,我们需要使用栈来临时存储函数返回后要恢复的值。

函数需要使用的任何寄存器都应该将其当前值放入栈中,以便使用 PUSH 指令安全保存。然后,在函数完成其逻辑之后,这些寄存器可以使用 POP 指令恢复其原始值。这样便可保证在调用函数前后,寄存器中的任何值都没有变化。如果我们在函数中处理好这一点,我们就可以随意调用函数,而不用担心它们对寄存器做了什么更改。

CALLRET 指令也使用栈。当 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!

5. Lesson 5 多文件编程

外部包含文件 (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 中修复。


6. Lesson 6 字符串结束符

为什么 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.

7. Lesson 7 带换行符打印

换行符(Linefeeds)对于需要用户输入的程序很重要。但 换行符 维护起来可能很麻烦。有时你想把它们加在字符串中,有时你想删除它们。如果声明字符串变量时在其后添加了 0Ah,但代码中有一处地方我们不想打印出该变量的 换行符,那么需要编写一些额外的逻辑代码,在运行时将其从字符串中删除。

如果编写一个新的函数 sprintLF 来打印字符串+换行,那么对于程序的可维护性来说会更好。这样,就可以在需要打印换行符时调用这个新函数 sprintLF,而在不需要时调用 sprint

sys_write 的调用要求传递一个指向待打印字符串地址的指针,因此我们不能只向打印函数传递换行符 0Ah。但我们也不想仅仅为了保存换行符而创建另一个变量,于是我们使用栈(stack)。

将换行字符移动到 EAX 中,然后将 EAX push 到栈上,并获取 Extended Stack Pointer 指向的地址。ESP 是一个寄存器,当将元素推送到堆栈中时,ESP 将递减,以指向最后一个元素在内存中的地址,因此可以使用它直接从栈中访问该元素。所以 sys_write 能够通过 ESP0Ah 在内存中的地址打印换行符。

; 文件名: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.

8. Lesson 8 参数传递

从命令行向程序传递参数就像在 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

9. Lesson 9 用户输入

9.1 Introduction to the .bss section

到目前为止,我们已经使用了.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

9.2 Writing our program

我们将使用系统调用 sys_read 来接收和处理来自用户的输入。此函数在 Linux 系统调用表中指定为 OPCODE 3。就像 sys_write 一样,该函数也有3个参数,在请求调用该函数的软件中断之前,这些参数将被加载到 EDXECXEBX 中。

参数传递如下:

  • 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

10. Lesson 10 打印1-10

10.1 Background

汇编中数字计数并不像想象中的那样简单。首先,我们需要在内存中传递 sys_write 地址,这样我们就不能直接用数字来填充寄存器并调用 print 函数。其次,数字和字符串在汇编中是完全不同的东西。字符串由 ASCII 码表示。ASCII 代表美国信息交换标准码(American Standard Code for Information Interchange)。ASCII 的创建标准化了所有计算机上的字符表示。

记住,不能打印数字,必须打印字符串。我们需要将数字从标准整数转换为 ASCII 码表示。查看 ASCII 码表,可以看到数字 “1” 的字符表示实际上是 ASCII 中的“49”。故将 48 加到数字上,就可以将它们从整数转换为 ASCII 表示。


10.2 Writing our program

我们使用 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,因为每个位值都需要单独进行转换。

 类似资料: