Learn Assembly Language 汇编语言学习(拙译)

Index-0

原址:https://asmtutor.com/
环境:nasm on x64 linux

TL;DR

  • 动机:程序员——多掌握几门计算机语言,还是有好处的
  • 主题:汇编语言——有其不可替代的作用
  • 呈示:天下语言逾千——汇编笑看沉舟侧畔
  • 展开:欲知程序真相——反编译难,反汇编易
  • 再现:大道器也不器——初见时如茶味甘苦,洞悉后若灌顶醍醐;原以为听多说多皆已昨,忽回首似曾相识又如陌;罢,风流不在谈峰健,相对无言味更长……

原文作者自己说 『This project was put together to teach myself NASM assembly language on linux.

欸~,原来是很窄众的哦。

写的虽然通俗,但依然能感到其面向的并不是毫无编程基础的人群,所谓“某子不能隐真恶”,无论怎样努力的将大量概念、原理、知识安排到看似聊天般的文字中,这里都要提醒读者注意,提防因为好奇心而陷入递归学习的泥潭……


Lesson 1 Hello, world!

背景知识

汇编语言是一种低级语言,汇编程序员与底层硬件之间唯一的接口只有内核本身。用汇编语言编程,涉及到 Linux 内核提供的系统调用机制。这些系统调用是操作系统内置的库函数,提供诸如读取键盘输入以及将输出显示到屏幕之类的功能。

当用户程序发起系统调用时,内核将立即挂起该程序,进而通过驱动程序让相关硬件完成用户程序所发起的任务请求,最后,将控制权交还给用户程序。

提示

驱动程序的驱动二字,形象的描述了内核对硬件的控制

在汇编语言中发起系统调用,需要向EAX寄存器写入相应调用的函数编号(也即:操作码OPCODE),同时设置其他几个寄存器的值作为实际参数,一切准备停当后,指令INT发送一个软中断,内核收到中断请求后接受参数并执行相应的库函数。简单直接。

来写我们的第壹个汇编程序吧——美玉有瑕

还是从著名的例子——$$Hello, world! $$ 开始,我们的汇编程序将把这个让无数程序员产生我已经学会这种语言了的错觉的字符串打印到标准输出上。

首先,在数据段定义一个msg变量,并赋给其一个字符串类型的值作为程序的输出。而在代码段中,通过编写全局标签_start:,告诉内核我们(写的诗)程序开始的地方(没有远方)

实际参数通过以下寄存器传给内核:

  • EDX存储字符串的长度(字节数)
  • ECX存储字符串的首地址(定义在数据段中的msg变量加载到内存后所在的位置)
  • EBX存储字符串写操作的目标文件——本例中是STDOUT

数据类型和实际参数的含义可以在函数定义中查到。

1# https://github.com/torvalds/linux/blob/master/include/linux/syscalls.h
2asmlinkage long sys_write(unsigned int fd, const char __user *buf, size_t count);

接下来,编译、链接、运行程序

 1; Hello World Program - asmtutor.com
 2; Compile with: nasm -f elf helloworld.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld.o -o helloworld
 4; Run with: ./helloworld
 5 
 6SECTION .data
 7msg     db      'Hello World!', 0Ah     ; assign msg variable with your message string
 8 
 9SECTION .text
10global  _start
11 
12_start:
13 
14    mov     edx, 13     ; number of bytes to write - one for each letter plus 0Ah (line feed character)
15    mov     ecx, msg    ; move the memory address of our message string into ecx
16    mov     ebx, 1      ; write to the STDOUT file
17    mov     eax, 4      ; invoke SYS_WRITE (kernel opcode 4)
18    int     80h
1$ nasm -f elf helloworld.asm
2$ ld -m elf_i386 helloworld.o -o helloworld
3$ ./helloworld
4Hello World!
5Segmentation fault
错误

系统报告了错误——Segmentation fault


Lesson 2 程序退出的正确姿势

若干背景知识

让我们从第一课成功发起系统调用的短暂喜悦中回过神来,学习内核中另一个最重要的系统调用sys_exit

还记得,上一课中,程序运行并打印了 $$Hello, world!$$ 字符串后,还看到了一句Segmentation fault

嘛,计算机程序可看作是装载到内存中且被分割成若干节(或段)的一长条的指令序列,这个通用的内存池实际上被所有程序共享,保存着变量、指令,其他程序等等……每一个段都有一个地址,以便其中存储的二进制信息之后的定位访问。

要执行加载到内存中的程序,我们使用全局标签_start:来告诉操作系统从哪里找到并开始执行我们的程序。从那个位置开始,内存将依据程序的逻辑所决定的下一个地址被依次访问。内核在这些地址上愉悦的跳来跳去,执行着程序。

与告诉内核一个程序从哪里开始同样重要的是:程序在哪里结束。这正是上一课中的程序所缺少的步骤。因为这个重要步骤的缺失,在调用完sys_write,内核把控制权交还给我们的程序之后,程序继续顺序执行内存中紧挨在int 80h之后的地址中的"指令"(天知道那一刻那里存的是啥),我们不知道内核将执行什么指令,但显然在这个例子中内核噎住了,并且不高兴(非正常)的终止了进程,严肃的招待了我们一个:Segmentation fault

在程序的末尾调用sys_exit吧!

开写我们的第贰个汇编程序——善始善终

sys_exit的定义简单明了。在Linux 系统调用表中,操作码OPCODE 1被分配给了她,同时她比sys_write节省一些,调用她只需要传一个参数

1# https://github.com/torvalds/linux/blob/master/include/linux/syscalls.h
2asmlinkage long sys_exit(int error_code);

她要被要这样调:

  • EBX里存0意为零个错误
  • EAX当然就存1了 (sys_exit 的 OPCODE)
  • 然后,软中断INT 80h和上一个例子一样
 1; Hello World Program - asmtutor.com
 2; Compile with: nasm -f elf helloworld.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld.o -o helloworld
 4; Run with: ./helloworld
 5 
 6SECTION .data
 7msg     db      'Hello World!', 0Ah
 8 
 9SECTION .text
10global  _start
11 
12_start:
13 
14    mov     edx, 13
15    mov     ecx, msg
16    mov     ebx, 1
17    mov     eax, 4
18    int     80h
19 
20    mov     ebx, 0      ; return 0 status on exit - 'No Errors'
21    mov     eax, 1      ; invoke SYS_EXIT (kernel opcode 1)
22    int     80h
1$ nasm -f elf helloworld.asm
2$ ld -m elf_i386 helloworld.o -o helloworld
3$ ./helloworld
4Hello World!

$$We\ will\ meet\ again,\ Segmentation\ fault$$


Lesson 3 计算字符串长度

又一些背景知识

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

嘛,sys_write必须知道我们传给她的字符串的指针和长度(字节数),才能够打印输出。如果修改了msg字符串的内容,也必须相应的更新字符串的长度,否则打印操作将不正确。

为了验证这一点,我们用第二课中的例子。将msg字符串修改为:(中括号帮助标识边界用) $$[Hello,\ brave\ new\ world!]$$ 编译、链接、执行修改后的程序。输出变为: $$[Hello,\ brave\ ]$$ (仅有前 13 个 ascii 字符,空格也算),这是因为我们没有将长度实参的值从原来的 13,更新为新字符串长度 23

我们的第叁个汇编程序登场——魔尺道丈

要计算某个字符串的长度,这里引入一种称作指针算数的技术。具体步骤为:

  • 选两个寄存器初始化为相同的内存地址
  • 用其中一个寄存器(本例中使用EAX)遍历要输出的字符串中的字符,在每次遇到 1 个字符的时候给自己加 1,直到其遇到一个代表字符串结尾的特殊字符
  • 此时用EAX减去一开始初始化为相同值的另一个寄存器的值,结果就是字符的个数

有点像两个数组做减法,差表示了两个地址之间的元素的个数。我们用这个差值替代旧例子中的硬编码值,传递给sys_write

汇编程序中,通常用CMP指令进行某种判断,根据其两个操作数的比较结果来置位标志寄存器,后续指令根据标志寄存器的值来决定如何推进程序的流程。

在接下来的代码中,我们关注的是ZF (Zero Flag)标志寄存器。如果EAX寄存器中的地址所指向的字符 ascii 值等于 0,则ZF被置位。而后JZ指令看到ZF为 1,就跳转到其操作数所指明的位置(流程改变),这个跳转为的是退出 nextchar 循环从而执行后续的程序代码

 1; Hello World Program (Calculating string length)
 2; Compile with: nasm -f elf helloworld-len.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld-len.o -o helloworld-len
 4; Run with: ./helloworld-len
 5 
 6SECTION .data
 7msg     db      'Hello, brave new world!', 0Ah ; we can modify this now without having to update anywhere else in the program
 8 
 9SECTION .text
10global  _start
11 
12_start:
13 
14    mov     ebx, msg        ; move the address of our message string into EBX
15    mov     eax, ebx        ; move the address in EBX into EAX as well (Both now point to the same segment in memory)
16 
17nextchar:
18    cmp     byte [eax], 0   ; compare the byte pointed to by EAX at this address against zero (Zero is an end of string delimiter)
19    jz      finished        ; jump (if the zero flagged has been set) to the point in the code labeled 'finished'
20    inc     eax             ; increment the address in EAX by one byte (if the zero flagged has NOT been set)
21    jmp     nextchar        ; jump to the point in the code labeled 'nextchar'
22 
23finished:
24    sub     eax, ebx        ; subtract the address in EBX from the address in EAX
25                            ; remember both registers started pointing to the same address (see line 15)
26                            ; but EAX has been incremented one byte for each character in the message string
27                            ; when you subtract one memory address from another of the same type
28                            ; the result is number of segments between them - in this case the number of bytes
29 
30    mov     edx, eax        ; EAX now equals the number of bytes in our string
31    mov     ecx, msg        ; the rest of the code should be familiar now
32    mov     ebx, 1
33    mov     eax, 4
34    int     80h
35 
36    mov     ebx, 0
37    mov     eax, 1
38    int     80h
1$ nasm -f elf helloworld-len.asm
2$ ld -m elf_i386 helloworld-len.o -o helloworld-len
3$ ./helloworld-len
4Hello, brave new world!

Lesson 4 子例程

引入子例程

子例程——函数也。他们是可复用的代码片段,能够被用户程序调用完成各种各样的任务。和前面定义程序入口点一样,子例程也通过定义标签来声明其起始位置(如strlen:),然而不同之处在于程序不使用JMP指令来访问子例程,取而代之的是使用CALL指令。同样,子例程执行完成后的跳转回地址也不使用JMP而是使用RET

同样是跳转到指令的所在处(memory address),为什么子例程不使用JMP呢?

子例程的威力在于其可复用性,想在程序中的任意位置随时调用子例程,除了要跳转到子例程所在的地址外,还必须要编写一些逻辑来确定子例程运行完后跳转回的位置,如果使用JMP将导致我们的代码中到处是非必要的标签。而使用CALLRET,汇编语言将采用称作堆栈的机制代为处理这些细节。

引入栈

栈也在内存中,然而某个程序的栈内存对其而言具备一些特殊性质。栈内存的存取遵循后进先出 Last In First Out memory (LIFO)原则。可以将其想象成厨房中的一摞碟子,最后一个放在顶上的碟子也正是下一次用碟子时第一个被取走的。

诚然,汇编中的栈内存放不下碟子,但是能放二进制数据。变量、地址、甚至其他程序都可以放进去。当调用子例程时,我们需要使用栈来临时存放上述数据以便子例程使用。

通常,执行中的一段代码所使用着的任何寄存器,都应该在调用子例程之前,使用PUSH指令将其中的数据压入栈,以此确保子例程返回后可以还原这些寄存器的原有值(因为子例程有可能会使用上述寄存器存储新的数据,不预先保存的话被覆盖后就丢失了),还原通过与PUSH指令执行顺序相反的顺序执行POP指令来完成。如此,就不必担心子例程执行过程中对上述寄存的修改。

CALLRET两个指令与PUSHPOP相似,也使用到了堆栈,但他们除了压栈/弹栈外还做了额外的工作,当CALL一个子例程时,CALL指令所在位置的下一个内存地址(return address)被压入栈,同时子例程所在地址被存入EIP。这个存在栈内存中的地址(return address)将由在子例程中的RET指令弹给EIP从而跳转回调用者的代码继续执行。希望这段描述能够些许的消除【内联标签使用JMP,而子例程(函数)调用使用CALL】的疑惑。

我们的第肆个汇编程序——管中窥豹

 1; Hello World Program (Subroutines)
 2; Compile with: nasm -f elf helloworld-len.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld-len.o -o helloworld-len
 4; Run with: ./helloworld-len
 5 
 6SECTION .data
 7msg     db      'Hello, brave new world!', 0Ah
 8 
 9SECTION .text
10global  _start
11 
12_start:
13 
14    mov     eax, msg        ; move the address of our message string into EAX
15    call    strlen          ; call our function to calculate the length of the string
16 
17    mov     edx, eax        ; our function leaves the result in EAX
18    mov     ecx, msg        ; this is all the same as before
19    mov     ebx, 1
20    mov     eax, 4
21    int     80h
22 
23    mov     ebx, 0
24    mov     eax, 1
25    int     80h
26 
27strlen:                     ; this is our first function declaration
28    push    ebx             ; push the value in EBX onto the stack to preserve it while we use EBX in this function
29    mov     ebx, eax        ; move the address in EAX into EBX (Both point to the same segment in memory)
30 
31nextchar:                   ; this is the same as lesson3
32    cmp     byte [eax], 0
33    jz      finished
34    inc     eax
35    jmp     nextchar
36 
37finished:
38    sub     eax, ebx
39    pop     ebx             ; pop the value on the stack back into EBX
40    ret                     ; return to where the function was called
1$ nasm -f elf helloworld-len.asm
2$ ld -m elf_i386 helloworld-len.o -o helloworld-len
3$ ./helloworld-len
4Hello, brave new world!

Lesson 5 外部包含文件

外部包含文件使得我们能够将程序的代码分散不同文件中。对于撰写清晰、易维护的程序来说这个技术很有用。可重用代码能够写成子例程保存在分散的文件中,这类文件被称为库。当你想使用库中的某段代码时,包含该库文件到你的程序,就好像该文件的内容是就是你程序的一部分一样。

本节我们把计算字符串长度的子例程移到外部文件中。同时,将字符串打印和程序退出逻辑也都修缮为子例程一并移到外部文件里。如此,瘦身后的程序看起来更加清晰、易读。

这里多声明一条消息,调用两次字符串打印子例程来演示对代码的复用。

提示

之后的课程中,除非 functions.asm 发生了改变,否则其代码将被省略

我们的第伍个汇编程序——他山之石

 1;------------------------------------------
 2; functions.asm
 3;
 4
 5; int slen(String message)
 6; String length calculation function
 7slen:
 8    push    ebx
 9    mov     ebx, eax
10 
11nextchar:
12    cmp     byte [eax], 0
13    jz      finished
14    inc     eax
15    jmp     nextchar
16 
17finished:
18    sub     eax, ebx
19    pop     ebx
20    ret
21 
22 
23;------------------------------------------
24; void sprint(String message)
25; String printing function
26sprint:
27    push    edx
28    push    ecx
29    push    ebx
30    push    eax
31    call    slen
32 
33    mov     edx, eax
34    pop     eax
35 
36    mov     ecx, eax
37    mov     ebx, 1
38    mov     eax, 4
39    int     80h
40 
41    pop     ebx
42    pop     ecx
43    pop     edx
44    ret
45 
46 
47;------------------------------------------
48; void exit()
49; Exit program and restore resources
50quit:
51    mov     ebx, 0
52    mov     eax, 1
53    int     80h
54    ret
 1; Hello World Program (External file include)
 2; Compile with: nasm -f elf helloworld-inc.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld-inc.o -o helloworld-inc
 4; Run with: ./helloworld-inc
 5 
 6%include        'functions.asm'                             ; include our external file
 7 
 8SECTION .data
 9msg1    db      'Hello, brave new world!', 0Ah              ; our first message string
10msg2    db      'This is how we recycle in NASM.', 0Ah      ; our second message string
11 
12SECTION .text
13global  _start
14 
15_start:
16 
17    mov     eax, msg1       ; move the address of our first message string into EAX
18    call    sprint          ; call our string printing function
19 
20    mov     eax, msg2       ; move the address of our second message string into EAX
21    call    sprint          ; call our string printing function
22 
23    call    quit            ; call our quit function
1$ nasm -f elf helloworld-inc.asm
2$ ld -m elf_i386 helloworld-inc.o -o helloworld-inc
3$ ./helloworld-inc
4Hello, brave new world!
5This is how we recycle in NASM.
6This is how we recycle in NASM.
错误

貌似第二条消息被打印了两次,我们在下节课修正:-)


Lesson 6 NULL 终止符

好吧,上一节的结尾我用“貌似”二字修饰了msg2被打印了两次这一现象,实际上程序并没有逻辑错误,她忠实的履行了职责,完成了我们的任务委托,也即:上一节的代码写法,输出就应该是那样子的。解释现象之前,先分别注释掉打印msg1msg2的代码,只留其中一个看看效果

如果,只注释msg1的打印指令,

1;   mov     eax, msg1       ; move the address of our first message string into EAX
2;   call    sprint          ; call our string printing function
3    
4    mov     eax, msg2       ; move the address of our second message string into EAX
5    call    sprint          ; call our string printing function

输出为:

1This is how we recycle in NASM.

输出和我们预期的相符。

如果,只注释msg2的打印指令,

1    mov     eax, msg1       ; move the address of our first message string into EAX
2    call    sprint          ; call our string printing function
3    
4;   mov     eax, msg2       ; move the address of our second message string into EAX
5;   call    sprint          ; call our string printing function

输出为:

1Hello, brave new world!
2This is how we recycle in NASM.

等一下,打印msg1的指令,怎么把第二个字符串也打印了?

答案在于,对于msg1字符串,我们没有给出明确的结尾。在数据段中的两条相邻的db代码,内存的分配也是相邻的,因此,msg1字符串的最后一个字节紧挨着msg2的第一个字节。记得之前,计算字符串长度所采用的方法——通过比较值为 0 的字节作为结尾!Lesson 5 的代码在运行到计算msg1的长度时,子例程读完了所有msg1的字节后,并没有遇到值为 0 的字节,所以她认为这个字符串还没完,程序继续从msg2的第一个字节逐个比较,直到msg2的所有字节也读完了,才找到字符串结尾。

因此看上去msg2被打印两次,实际上是视觉上的问题,实际的输出过程是:

先输出了Hello, brave new world!\nThis is how we recycle in NASM.\n 又输出了This is how we recycle in NASM.\n

再等一下,你或许会问,msg2我也没看到有字符串结尾啊?被你逮到了:-)这是因为数据段的内存在开辟时,都被初始化成了 0h。

我们的第陆个汇编程序——楚河汉界

提示

汇编程序中,用 0h 表示 NULL 字节,而 NULL 字节标识了字符串的结尾

functions.asm 略

 1; Hello World Program (NULL terminating bytes)
 2; Compile with: nasm -f elf helloworld-inc.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld-inc.o -o helloworld-inc
 4; Run with: ./helloworld-inc
 5 
 6%include        'functions.asm'
 7 
 8SECTION .data
 9msg1    db      'Hello, brave new world!', 0Ah, 0h          ; NOTE the null terminating byte
10msg2    db      'This is how we recycle in NASM.', 0Ah, 0h  ; NOTE the null terminating byte
11 
12SECTION .text
13global  _start
14 
15_start:
16 
17    mov     eax, msg1
18    call    sprint
19 
20    mov     eax, msg2
21    call    sprint
22 
23    call    quit
1$ nasm -f elf helloworld-inc.asm
2$ ld -m elf_i386 helloworld-inc.o -o helloworld-inc
3$ ./helloworld-inc
4Hello, brave new world!
5This is how we recycle in NASM.

Lesson 7 换行

换行的重要性对于控制台程序而言是不言而喻的,尤其是构建需要用户输入的程序时,更是如此。但是换行又是难以摆弄的,处理字符串时有时需要包含换行,有时又需要去掉他。如果始终把表示换行符的 ascii 码 0Ah 硬编码在我们的变量中,就要面对一个问题——在不需要输出换行符的地方得写额外的代码来去掉他。

如果有个专门的子例程输出我们指定的字符串,由该子例程负责在结尾打印一个换行符。就可以在需要打印换行符的地方调用这个子例程,而在不需要打印换行符的地方还使用我们的sprint子例程就可以了。

记得前文中,想调用sys_write必须提供要打印内容的地址及其长度,所以仅仅传递换行符是不够的,同时我们也不想仅仅为了这一个字符单独定义一个变量来保存他,因此使用栈内存来实现新的子例程。

招法如下:

  • 换行符存进EAX
  • PUSH EAX的值到栈上,并获取ESP的值(当前栈顶所在的地址)
  • 通过当前的ESP知道了换行符所在内存地址,那么调用sys_write的必要条件已然具备

且看我们的第柒个汇编程序——路转峰回

提示

观察 functions.asm 中的 sprintLF 子例程

 1;------------------------------------------
 2; int slen(String message)
 3; String length calculation function
 4slen:
 5    push    ebx
 6    mov     ebx, eax
 7 
 8nextchar:
 9    cmp     byte [eax], 0
10    jz      finished
11    inc     eax
12    jmp     nextchar
13 
14finished:
15    sub     eax, ebx
16    pop     ebx
17    ret
18 
19 
20;------------------------------------------
21; void sprint(String message)
22; String printing function
23sprint:
24    push    edx
25    push    ecx
26    push    ebx
27    push    eax
28    call    slen
29 
30    mov     edx, eax
31    pop     eax
32 
33    mov     ecx, eax
34    mov     ebx, 1
35    mov     eax, 4
36    int     80h
37 
38    pop     ebx
39    pop     ecx
40    pop     edx
41    ret
42 
43 
44;------------------------------------------
45; void sprintLF(String message)
46; String printing with line feed function
47sprintLF:
48    call    sprint
49 
50    push    eax         ; push eax onto the stack to preserve it while we use the eax register in this function
51    mov     eax, 0Ah    ; move 0Ah into eax - 0Ah is the ascii character for a linefeed
52    push    eax         ; push the linefeed onto the stack so we can get the address
53    mov     eax, esp    ; move the address of the current stack pointer into eax for sprint
54    call    sprint      ; call our sprint function
55    pop     eax         ; remove our linefeed character from the stack
56    pop     eax         ; restore the original value of eax before our function was called
57    ret                 ; return to our program
58 
59 
60;------------------------------------------
61; void exit()
62; Exit program and restore resources
63quit:
64    mov     ebx, 0
65    mov     eax, 1
66    int     80h
67    ret
 1; Hello World Program (Print with line feed)
 2; Compile with: nasm -f elf helloworld-lf.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld-lf.o -o helloworld-lf
 4; Run with: ./helloworld-lf
 5 
 6%include        'functions.asm'
 7 
 8SECTION .data
 9msg1    db      'Hello, brave new world!', 0h          ; NOTE we have removed the line feed character 0Ah
10msg2    db      'This is how we recycle in NASM.', 0h  ; NOTE we have removed the line feed character 0Ah
11 
12SECTION .text
13global  _start
14 
15_start:
16 
17    mov     eax, msg1
18    call    sprintLF    ; NOTE we are calling our new print with linefeed function
19 
20    mov     eax, msg2
21    call    sprintLF    ; NOTE we are calling our new print with linefeed function
22 
23    call    quit
1$ nasm -f elf helloworld-lf.asm
2$ ld -m elf_i386 helloworld-lf.o -o helloworld-lf
3$ ./helloworld-lf
4Hello, brave new world!
5This is how we recycle in NASM.

Lesson 8 命令行参数

在 NASM 中,接收命令行参数也使用堆栈。程序启动时,所有参数被反序的压入堆栈,然后程序名被压入栈,最后是参数的个数被压入栈。对于 NASM 编写的程序,最顶上的两个栈内存单元总是保存着程序名和参数个数。

要处理这些参数,我们所要做的就是执行若干次POP逐个弹出参数信息,然后迭代每一个参数并运行我们的程序逻辑。在本例中,简单的调用sprintLF函数打印输出。

注意

我们使用ECX寄存器作为循环计数器。这个通用寄存器最初的设计意图就是计数。

我们的第捌个汇编程序——兵来将挡

functions.asm 略

 1; Hello World Program (Passing arguments from the command line)
 2; Compile with: nasm -f elf helloworld-args.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld-args.o -o helloworld-args
 4; Run with: ./helloworld-args
 5 
 6%include        'functions.asm'
 7 
 8SECTION .text
 9global  _start
10 
11_start:
12 
13    pop     ecx             ; first value on the stack is the number of arguments
14 
15nextArg:
16    cmp     ecx, 0h         ; check to see if we have any arguments left
17    jz      noMoreArgs      ; if zero flag is set jump to noMoreArgs label (jumping over the end of the loop)
18    pop     eax             ; pop the next argument off the stack
19    call    sprintLF        ; call our print with linefeed function
20    dec     ecx             ; decrease ecx (number of arguments left) by 1
21    jmp     nextArg         ; jump to nextArg label
22 
23noMoreArgs:
24    call    quit
1$ nasm -f elf helloworld-args.asm
2$ ld -m elf_i386 helloworld-lf.o -o helloworld-args
3$ ./helloworld-args "This is one argument" "This is another" 101
4./helloworld-args
5This is one argument
6This is another
7101

Lesson 9 处理用户输出

引入 bss 段

目前为止,我们用到了代码段.text和数据段.data。接下来引入.bss段——全称 BLOCK Started by Symbol。这块内存用来保存程序中的未初始化变量。这些预留的空间通常用来存储用户输入的数据,这类数据的特点在于无法在编程时知道其具体的大小。

变量声明语法如下:

1SECTION .bss
2variableName1:      RESB    1       ; reserve space for 1 byte
3variableName2:      RESW    1       ; reserve space for 1 word
4variableName3:      RESD    1       ; reserve space for 1 double word
5variableName4:      RESQ    1       ; reserve space for 1 double precision float (quad word)
6variableName5:      REST    1       ; reserve space for 1 extended precision float

是时候引入另一个重要的系统调用sys_read了,让用户输入来的更猛烈些吧。在 Linux 系统调用表中,此函数的OPCODE 3。和sys_write一样也接收 3 个参数,详情如下:

  • EDX载入读取的最大长度(以字节为单位)
  • ECX载入.bss中创建的变量的地址
  • EBX载入要读取的文件描述符(在本例中为STDIN)

同样,函数参数的数据类型和含义可在行数定义中找到

1# https://github.com/torvalds/linux/blob/master/include/linux/syscalls.h
2asmlinkage long sys_read(unsigned int fd, char __user *buf, size_t count);

sys_read的行为:一旦读到一个LF,就返回到调用者程序,此时他至今为止读到的内容被存放在ECX中保存的地址所指向的内存中。

延伸阅读——EOF,到底怎么回事

这是我们的第玖个汇编程序——不预则废

functions.asm 略

 1; Hello World Program (Getting input)
 2; Compile with: nasm -f elf helloworld-input.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld-input.o -o helloworld-input
 4; Run with: ./helloworld-input
 5 
 6%include        'functions.asm'
 7 
 8SECTION .data
 9msg1        db      'Please enter your name: ', 0h      ; message string asking user for input
10msg2        db      'Hello, ', 0h                       ; message string to use after user has entered their name
11 
12SECTION .bss
13sinput:     resb    255                                 ; reserve a 255 byte space in memory for the users input string
14 
15SECTION .text
16global  _start
17 
18_start:
19 
20    mov     eax, msg1
21    call    sprint
22 
23    mov     edx, 255        ; number of bytes to read
24    mov     ecx, sinput     ; reserved space to store our input (known as a buffer)
25    mov     ebx, 0          ; read from the STDIN file
26    mov     eax, 3          ; invoke SYS_READ (kernel opcode 3)
27    int     80h
28 
29    mov     eax, msg2
30    call    sprint
31 
32    mov     eax, sinput     ; move our buffer into eax (Note: input contains a linefeed)
33    call    sprint          ; call our print function
34 
35    call    quit
1$ nasm -f elf helloworld-input.asm
2$ ld -m elf_i386 helloworld-input.o -o helloworld-input
3$ ./helloworld-input
4Please enter your name: Daniel Givney
5Hello, Daniel Givney

Lesson 10 数到十

又来了,背景知识

与直觉上相反,在汇编语言中计数并不那么简单直接。首先,我们得传递一个地址给sys_write,不能仅仅将数字加载到寄存器中并调用我们的print函数。其次,数字和字符串在汇编里大不相同。字符串被描述为一系列的 ASCII 值。这里有一个关于 ASCII 的一个不错的站点(怎么打开净是广告。。。) ,这套编码被用来规范跨计算机的字符串表示的统一标准。

Remember,是无法打印的——打印出来的都是字符串。为了数到 10,需要从标准的整数到对应的 ASCII 字符串的转换。看过 ASCII 码表之后,可以注意到,整数 1 对应的 ASCII 值是 49。实际上,对于 0 到 9 这 10 个整数,加 48 就是其对应的 ASCII 码。

我们的第拾个汇编程序——邯郸学步

functions.asm 略

 1; Hello World Program (Count to 10)
 2; Compile with: nasm -f elf helloworld-10.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld-10.o -o helloworld-10
 4; Run with: ./helloworld-10
 5 
 6%include        'functions.asm'
 7 
 8SECTION .text
 9global  _start
10 
11_start:
12 
13    mov     ecx, 0          ; ecx is initalised to zero.
14 
15nextNumber:
16    inc     ecx             ; increment ecx
17 
18    mov     eax, ecx        ; move the address of our integer into eax
19    add     eax, 48         ; add 48 to our number to convert from integer to ascii for printing
20    push    eax             ; push eax to the stack
21    mov     eax, esp        ; get the address of the character on the stack
22    call    sprintLF        ; call our print function
23 
24    pop     eax             ; clean up the stack so we don't have unneeded bytes taking up space
25    cmp     ecx, 10         ; have we reached 10 yet? compare our counter with decimal 10
26    jne     nextNumber      ; jump if not equal and keep counting
27 
28    call    quit
 1$ nasm -f elf helloworld-10.asm
 2$ ld -m elf_i386 helloworld-10.o -o helloworld-10
 3$ ./helloworld-10
 41
 52
 63
 74
 85
 96
107
118
129
13:
错误

哦噢:我们的数字 10 打印成了冒号(:),咋了呢?


Lesson 11 数到十 (itoa)

注意

是 itoa,不是 iota

为什么 Lesson 10 的程序把 10 打印成了冒号(:)呢。嗯,让我们翻开 ASCII 表,能够看到冒号的 ASCII 值是 58 = 10 + 48,所以按上节的程序写法,就应该输出冒号并没有错。上节所说的 0 到 9 这 10 个整数,可以加 48 得到对应的 ASCII 值,然后传给sys_write的输出对应的字符串;但是对于数字 10,他有两位,没有一个单独的 ASCII 来表示 10 的字符串形式。这个两位数需要两个 ASCII,一个表示 1,一个表示 0。因此,传给sys_write一个'4948'才是数字 10 的正确字符串形式。10 直接加 48 不行,我们需要把数字的每一位除以 10 后逐个转换

这里引入两个新的子例程,iprintiprintLF。这些函数用来打印数字的字符串形式,数值本身使用EAX装载,ECX用来计数。然后重复除以 10 的过程,每次把余数加 48,结果值PUSH到栈上以备后用。当除以 10 的商(存在EAX里)为 0 时,将退出当前循环,进入另一个循环。在该循环里我们通过逐个POP弹栈的方法,打印每一位数字的字符串形式。弹到什么时候为止呢?这正是我们在ECX寄存器里存计数的目的,每弹一个值ECX就减 1,直到减到 0。所有的这些都完成后,程序退出。

除法指令简介

DIVIDIV指令将EAX中的值做为被除数,除以指令的原操作数,商的部分存到EAX(覆盖了原来的被除数)而余数部分存在EDX中。(作者写的挺简单,实现上复杂的多:不同位数的策略;高位存什么、低位存什么;有符号、无符号的区别等等。)

比如:

1mov     eax, 10         ; move 10 into eax
2mov     esi, 10         ; move 10 into esi
3idiv    esi             ; divide eax by esi (eax will equal 1 and edx will equal 0)
4idiv    esi             ; divide eax by esi again (eax will equal 0 and edx will equal 1)

只存余数,感觉会有问题?

不会,因为这些都是整数。即便除数比被除数大,依然能够在余数中找到其原值,这是因为在这种情况下EAX置 0,除法运算运行了 0 次,而被除数的原值作为余数被放到了EDX中。真好~

我们的第拾壹个汇编程序——知之非艰

注意

仅列出了新函数的注释

  1;------------------------------------------
  2; void iprint(Integer number)
  3; Integer printing function (itoa)
  4iprint:
  5    push    eax             ; preserve eax on the stack to be restored after function runs
  6    push    ecx             ; preserve ecx on the stack to be restored after function runs
  7    push    edx             ; preserve edx on the stack to be restored after function runs
  8    push    esi             ; preserve esi on the stack to be restored after function runs
  9    mov     ecx, 0          ; counter of how many bytes we need to print in the end
 10 
 11divideLoop:
 12    inc     ecx             ; count each byte to print - number of characters
 13    mov     edx, 0          ; empty edx
 14    mov     esi, 10         ; mov 10 into esi
 15    idiv    esi             ; divide eax by esi
 16    add     edx, 48         ; convert edx to it's ascii representation - edx holds the remainder after a divide instruction
 17    push    edx             ; push edx (string representation of an intger) onto the stack
 18    cmp     eax, 0          ; can the integer be divided anymore?
 19    jnz     divideLoop      ; jump if not zero to the label divideLoop
 20 
 21printLoop:
 22    dec     ecx             ; count down each byte that we put on the stack
 23    mov     eax, esp        ; mov the stack pointer into eax for printing
 24    call    sprint          ; call our string print function
 25    pop     eax             ; remove last character from the stack to move esp forward
 26    cmp     ecx, 0          ; have we printed all bytes we pushed onto the stack?
 27    jnz     printLoop       ; jump is not zero to the label printLoop
 28 
 29    pop     esi             ; restore esi from the value we pushed onto the stack at the start
 30    pop     edx             ; restore edx from the value we pushed onto the stack at the start
 31    pop     ecx             ; restore ecx from the value we pushed onto the stack at the start
 32    pop     eax             ; restore eax from the value we pushed onto the stack at the start
 33    ret
 34 
 35 
 36;------------------------------------------
 37; void iprintLF(Integer number)
 38; Integer printing function with linefeed (itoa)
 39iprintLF:
 40    call    iprint          ; call our integer printing function
 41 
 42    push    eax             ; push eax onto the stack to preserve it while we use the eax register in this function
 43    mov     eax, 0Ah        ; move 0Ah into eax - 0Ah is the ascii character for a linefeed
 44    push    eax             ; push the linefeed onto the stack so we can get the address
 45    mov     eax, esp        ; move the address of the current stack pointer into eax for sprint
 46    call    sprint          ; call our sprint function
 47    pop     eax             ; remove our linefeed character from the stack
 48    pop     eax             ; restore the original value of eax before our function was called
 49    ret
 50 
 51 
 52;------------------------------------------
 53; int slen(String message)
 54; String length calculation function
 55slen:
 56    push    ebx
 57    mov     ebx, eax
 58 
 59nextchar:
 60    cmp     byte [eax], 0
 61    jz      finished
 62    inc     eax
 63    jmp     nextchar
 64 
 65finished:
 66    sub     eax, ebx
 67    pop     ebx
 68    ret
 69 
 70 
 71;------------------------------------------
 72; void sprint(String message)
 73; String printing function
 74sprint:
 75    push    edx
 76    push    ecx
 77    push    ebx
 78    push    eax
 79    call    slen
 80 
 81    mov     edx, eax
 82    pop     eax
 83 
 84    mov     ecx, eax
 85    mov     ebx, 1
 86    mov     eax, 4
 87    int     80h
 88 
 89    pop     ebx
 90    pop     ecx
 91    pop     edx
 92    ret
 93 
 94 
 95;------------------------------------------
 96; void sprintLF(String message)
 97; String printing with line feed function
 98sprintLF:
 99    call    sprint
100 
101    push    eax
102    mov     eax, 0AH
103    push    eax
104    mov     eax, esp
105    call    sprint
106    pop     eax
107    pop     eax
108    ret
109 
110 
111;------------------------------------------
112; void exit()
113; Exit program and restore resources
114quit:
115    mov     ebx, 0
116    mov     eax, 1
117    int     80h
118    ret
 1; Hello World Program (Count to 10 itoa)
 2; Compile with: nasm -f elf helloworld-itoa.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 helloworld-itoa.o -o helloworld-itoa
 4; Run with: ./helloworld-itoa
 5 
 6%include        'functions.asm'
 7 
 8SECTION .text
 9global  _start
10 
11_start:
12 
13    mov     ecx, 0
14 
15nextNumber:
16    inc     ecx
17    mov     eax, ecx
18    call    iprintLF        ; NOTE call our new integer printing function (itoa)
19    cmp     ecx, 10
20    jne     nextNumber
21 
22    call    quit
 1$ nasm -f elf helloworld-itoa.asm
 2$ ld -m elf_i386 helloworld-itoa.o -o helloworld-itoa
 3$ ./helloworld-itoa
 41
 52
 63
 74
 85
 96
107
118
129
1310

Lesson 12 计算——加法

这次的程序,将寄存器EAXEBX中的值相加,和保存在EAX中。首先MOV其中一个加数到EAX(本例中为 90),然后MOV另一个加数到EBX(本例中为9)。我们需要调用ADD指令来实现加法运算。EBXEAX中的值将被加到一起,而结果和将被存回指令最左边的寄存器中(也就是本例的EAX)。最后调用我们引以为傲的数值->字符串打印函数来完成程序。

我们的第拾贰个汇编程序——聚沙成塔

functions.asm 略

 1; Calculator (Addition)
 2; Compile with: nasm -f elf calculator-addition.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 calculator-addition.o -o calculator-addition
 4; Run with: ./calculator-addition
 5 
 6%include        'functions.asm'
 7 
 8SECTION .text
 9global  _start
10 
11_start:
12 
13    mov     eax, 90     ; move our first number into eax
14    mov     ebx, 9      ; move our second number into ebx
15    add     eax, ebx    ; add ebx to eax
16    call    iprintLF    ; call our integer print with linefeed function
17 
18    call    quit
1$ nasm -f elf calculator-addition.asm
2$ ld -m elf_i386 calculator-addition.o -o calculator-addition
3$ ./calculator-addition
499

Lesson 13 计算——减法

这个程序与 Lesson 12 的唯一区别就是加法指令ADD,换成了减法指令SUB

我们的第拾叁个汇编程序——红衰翠减

functions.asm 略

 1; Calculator (Subtraction)
 2; Compile with: nasm -f elf calculator-subtraction.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 calculator-subtraction.o -o calculator-subtraction
 4; Run with: ./calculator-subtraction
 5 
 6%include        'functions.asm'
 7 
 8SECTION .text
 9global  _start
10 
11_start:
12 
13    mov     eax, 90     ; move our first number into eax
14    mov     ebx, 9      ; move our second number into ebx
15    sub     eax, ebx    ; subtract ebx from eax
16    call    iprintLF    ; call our integer print with linefeed function
17 
18    call    quit
1$ nasm -f elf calculator-subtraction.asm
2$ ld -m elf_i386 calculator-subtraction.o -o calculator-subtraction
3$ ./calculator-subtraction
481

Lesson 14 计算——乘法

在本程序中,我们将用EBX中的值与EAX中的值相乘。两个整数按照和 Lesson 12 一样的方式分别存入两个寄存器。这次的指令主角是MUL,他是 NASM 中为数不多的几个单操作数指令之一。MUL指令总是将其操作数与EAX中的值相乘,并将积存回EAX

我们的第拾肆个汇编程序——一登龙门

functions.asm 略

 1; Calculator (Multiplication)
 2; Compile with: nasm -f elf calculator-multiplication.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 calculator-multiplication.o -o calculator-multiplication
 4; Run with: ./calculator-multiplication
 5 
 6%include        'functions.asm'
 7 
 8SECTION .text
 9global  _start
10 
11_start:
12 
13    mov     eax, 90     ; move our first number into eax
14    mov     ebx, 9      ; move our second number into ebx
15    mul     ebx         ; multiply eax by ebx
16    call    iprintLF    ; call our integer print with linefeed function
17 
18    call    quit
1$ nasm -f elf calculator-multiplication.asm
2$ ld -m elf_i386 calculator-multiplication.o -o calculator-multiplication
3$ ./calculator-multiplication
4810

Lesson 15 计算——除法

除法指令我们已经见识过了,我们曾用他来实现数字值到 ASCII 码的转换。本例中,依然使用EAXEBX。他们分别保存被除数和除数。如前所述,DIV指令的行为如下:用其操作数去除EAX中的值,商保存到EAX,而余数保存到EDX。最后将商和余数都打印出来

又见除法的第拾伍个汇编程序——经分之术

functions.asm 略

 1; Calculator (Division)
 2; Compile with: nasm -f elf calculator-division.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 calculator-division.o -o calculator-division
 4; Run with: ./calculator-division
 5 
 6%include        'functions.asm'
 7 
 8SECTION .data
 9msg1        db      ' remainder '      ; a message string to correctly output result
10 
11SECTION .text
12global  _start
13 
14_start:
15 
16    mov     eax, 90     ; move our first number into eax
17    mov     ebx, 9      ; move our second number into ebx
18    div     ebx         ; divide eax by ebx
19    call    iprint      ; call our integer print function on the quotient
20    mov     eax, msg1   ; move our message string into eax
21    call    sprint      ; call our string print function
22    mov     eax, edx    ; move our remainder into eax
23    call    iprintLF    ; call our integer printing with linefeed function
24 
25    call    quit
1$ nasm -f elf calculator-division.asm
2$ ld -m elf_i386 calculator-division.o -o calculator-division
3$ ./calculator-division
410 remainder 0

Lesson 16 计算——atoi

将算数运算中的数值写死在代码里并不激动人心,这次我们要从命令行参数中动态获取变化的数值!

程序步骤

  • 我们先用POP从栈上获得命令行参数的个数,并保存在ECX寄存器;然后继续POP出命令的名称,从ECX中去掉他的计数;随后循环的弹出每个参数并执行加法逻辑。
  • 和输出一样,输入给我们的也都是字符串。在进行加法运算前,需要一个和输出时相反的转换操作:ASCII 码->数字值,没有这个步骤加法的结果将不正确。
  • 这个转换操作由引入的子例程 Ascii to Integer(atoi) 来完成。这个函数将 ASCII 码值对应的数字保存在EAX寄存器。每次我们都把EAX的值加到EDX里去。如果传递给我们的 ASCII 码不是表示 0-9 那是个数字的,就用 0 替代他。
  • 所有参数都经过转换并加到一起后,打印和并退出

atoi 原理

将 ASCII 码转为整数并非易事。还记得我们之前是怎么把整数转成 ASCII 码的?现在然要进行其逆运算。

首先,将字符串的地址移入ESI(著名的源址寄存器);然后,逐字节的遍历字符串(试着将每个字节看做是一个数字或十进制位)。对于每个数字,检测其值是否介于 48 到 57 之间( 0-9 的 ASCII 码),如果满足条件,就执行以下逻辑:

码值减去 48——得到该码值所表示的数字的十进制整数值,这个值存入EAX,然后EAX乘以 10,随着循环的进行,每一位的位权都在随着乘以 10 而提升,从而归到其所占的位上。

当所有的字节都按照上面的逻辑处理完成,返回结果值之前,还要进入最后的一步:由于最后一位数字是个位数,他不应该乘以 10,但面的逻辑却这样做了。我们需要简单的除以一次 10 来纠正这个错误。当然,如果传递给程序的参数不是整数,这个除法操作就省略掉。

接下来的程序中使用了 BL 寄存器,这里简单说下

通用寄存器的个数就没怎么变过,但是其位数却一扩再扩以提高CPU的计算能力。汇编指令中写BL的意思是,使用32位EBX寄存器的低8位,因为单个 ASCII 码值只需要一个字节就可以放下。如果使用整个32位寄存器来存储这一个字节的数据,那么其中的24位都对我们毫无意义。

对于EBX,其0-16bits段称为BX,而BX则包含BLBH(低8位,高8位),显然我们只需要BL

正向的学习汇编,往往从头开始捣寄存器的历史,名称的含义和位的大小等等。本教程是反向的,通过程序中用到的必要元素来追溯关键的概念和原理。完整的寄存器知识超出了本教程的范围,但之后的篇章将继续讨论,毕竟写汇编嘛,我们绕不过寄存器去 :-)

欢迎我们的第拾陆个汇编程序——反朴归真

注意

funcitons.asm 只列出了新引入的 atoi 子例程的代码

 1;------------------------------------------
 2; int atoi(Integer number)
 3; Ascii to integer function (atoi)
 4atoi:
 5    push    ebx             ; preserve ebx on the stack to be restored after function runs
 6    push    ecx             ; preserve ecx on the stack to be restored after function runs
 7    push    edx             ; preserve edx on the stack to be restored after function runs
 8    push    esi             ; preserve esi on the stack to be restored after function runs
 9    mov     esi, eax        ; move pointer in eax into esi (our number to convert)
10    mov     eax, 0          ; initialise eax with decimal value 0
11    mov     ecx, 0          ; initialise ecx with decimal value 0
12 
13.multiplyLoop:
14    xor     ebx, ebx        ; resets both lower and uppper bytes of ebx to be 0
15    mov     bl, [esi+ecx]   ; move a single byte into ebx register's lower half
16    cmp     bl, 48          ; compare ebx register's lower half value against ascii value 48 (char value 0)
17    jl      .finished       ; jump if less than to label finished
18    cmp     bl, 57          ; compare ebx register's lower half value against ascii value 57 (char value 9)
19    jg      .finished       ; jump if greater than to label finished
20 
21    sub     bl, 48          ; convert ebx register's lower half to decimal representation of ascii value
22    add     eax, ebx        ; add ebx to our interger value in eax
23    mov     ebx, 10         ; move decimal value 10 into ebx
24    mul     ebx             ; multiply eax by ebx to get place value
25    inc     ecx             ; increment ecx (our counter register)
26    jmp     .multiplyLoop   ; continue multiply loop
27 
28.finished:
29    cmp     ecx, 0          ; compare ecx register's value against decimal 0 (our counter register)
30    je      .restore        ; jump if equal to 0 (no integer arguments were passed to atoi)
31    mov     ebx, 10         ; move decimal value 10 into ebx
32    div     ebx             ; divide eax by value in ebx (in this case 10)
33 
34.restore:
35    pop     esi             ; restore esi from the value we pushed onto the stack at the start
36    pop     edx             ; restore edx from the value we pushed onto the stack at the start
37    pop     ecx             ; restore ecx from the value we pushed onto the stack at the start
38    pop     ebx             ; restore ebx from the value we pushed onto the stack at the start
39    ret
 1; Calculator (ATOI)
 2; Compile with: nasm -f elf calculator-atoi.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 calculator-atoi.o -o calculator-atoi
 4; Run with: ./calculator-atoi 20 1000 317
 5 
 6%include        'functions.asm'
 7 
 8SECTION .text
 9global  _start
10 
11_start:
12 
13    pop     ecx             ; first value on the stack is the number of arguments
14    pop     edx             ; second value on the stack is the program name (discarded when we initialise edx)
15    sub     ecx, 1          ; decrease ecx by 1 (number of arguments without program name)
16    mov     edx, 0          ; initialise our data register to store additions
17 
18nextArg:
19    cmp     ecx, 0h         ; check to see if we have any arguments left
20    jz      noMoreArgs      ; if zero flag is set jump to noMoreArgs label (jumping over the end of the loop)
21    pop     eax             ; pop the next argument off the stack
22    call    atoi            ; convert our ascii string to decimal integer
23    add     edx, eax        ; perform our addition logic
24    dec     ecx             ; decrease ecx (number of arguments left) by 1
25    jmp     nextArg         ; jump to nextArg label
26 
27noMoreArgs:
28    mov     eax, edx        ; move our data result into eax for printing
29    call    iprintLF        ; call our integer printing with linefeed function
30    call    quit            ; call our q
1$ nasm -f elf calculator-atoi.asm
2$ ld -m elf_i386 calculator-atoi.o -o calculator-atoi
3$ ./calculator-atoi 20 1000 317
41337

Lesson 17 命名空间

对于任何包含大量代码库的软件项目,命名空间都是不可或缺的构造。命名空间为标识符引入作用域的概念,使得重用命名约定成为可能,同时增强了代码的可读性和可维护性。在汇编语言中,全局标签标识子例程,而局部标签用来实现命名空间。

之前的教程中,我们都只使用了全局标签。这意味着即便是实现相同逻辑的代码块,也必须有全局唯一的标签名,前面的finished标签就是一例。同属一个全局作用域,意味着当在一个函数中需要跳出循环时,直接JMP到该函数中的finished标签就行。然而,如果需要从另一个函数中跳出循环,和finished代码块功能相同的代码就得改名。我们希望能重用finished这个名字,因为他能向代码的阅读者暗示出某种已知的逻辑。

局部标签以.开头,比如:.finished。随着 functions.asm 的不断扩展,你可能已经注意到他的出现了。一个局部标签的命名空间,由离他最近的前面的一个全局标签限定。你可以使用JMP跳转到局部标签,编译器将通过当前调用的作用域(基于其之上的全局标签)计算出应该引用哪一个局部标签。

第拾柒个汇编程序——各安生业

functions.asm 略

 1; Namespace
 2; Compile with: nasm -f elf namespace.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 namespace.o -o namespace
 4; Run with: ./namespace
 5 
 6%include        'functions.asm'
 7 
 8SECTION .data
 9msg1        db      'Jumping to finished label.', 0h        ; a message string
10msg2        db      'Inside subroutine number: ', 0h        ; a message string
11msg3        db      'Inside subroutine "finished".', 0h     ; a message string
12 
13SECTION .text
14global  _start
15 
16_start:
17 
18subrountineOne:
19    mov     eax, msg1       ; move the address of msg1 into eax
20    call    sprintLF        ; call our string printing with linefeed function
21    jmp     .finished       ; jump to the local label under the subrountineOne scope
22 
23.finished:
24    mov     eax, msg2       ; move the address of msg2 into eax
25    call    sprint          ; call our string printing function
26    mov     eax, 1          ; move the value one into eax (for subroutine number one)
27    call    iprintLF        ; call our integer printing function with linefeed function
28 
29subrountineTwo:
30    mov     eax, msg1       ; move the address of msg1 into eax
31    call    sprintLF        ; call our string print with linefeed function
32    jmp     .finished       ; jump to the local label under the subrountineTwo scope
33 
34.finished:
35    mov     eax, msg2       ; move the address of msg2 into eax
36    call    sprint          ; call our string printing function
37    mov     eax, 2          ; move the value two into eax (for subroutine number two)
38    call    iprintLF        ; call our integer printing function with linefeed function
39 
40    mov     eax, msg1       ; move the address of msg1 into eax
41    call    sprintLF        ; call our string printing with linefeed function
42    jmp     finished        ; jump to the global label finished
43 
44finished:
45    mov     eax, msg3       ; move the address of msg3 into eax
46    call    sprintLF        ; call our string printing with linefeed function
47    call    quit            ; call our quit function
1$ nasm -f elf namespace.asm
2$ ld -m elf_i386 namespace.o -o namespace
3$ ./namespace
4Jumping to finished label.
5Inside subroutine number: 1
6Jumping to finished label.
7Inside subroutine number: 2
8Jumping to finished label.
9Inside subroutine "finished".

Lesson 18 FizzBuzz 游戏

FizzBuzz——老外在学校里教孩子除法的一组游戏。玩法:

  • 玩家轮流计算从1100的整数,
  • 遇到3的倍数就换成 Fizz,
  • 遇到5的倍数就换成 Buzz,
  • 遇到3和5的公倍数就换成 FizzBuzz。

神奇的是这个儿童游戏已经成为计算机编程工作事实上的标准面试题之一,原因在于该题可以轻松的过滤掉连简单的逻辑门都构造不好的候选人。。。

解题办法不止一种,有些语言提供了非常简单而优雅的方案。然而大都是躲不过 if 语句的;可能还会含有 else,这取决于是否利用了某些数学性质,如:既能被 3 整除也能被 5 整除的数必然可以被 3 * 5 = 15 整除。

本篇的汇编实现,我们提供如下的方案:两个级联的 if 语句判断是否打印 Fizz 和/或 Buzz,以及一个 else 语句打印其他数字。每个输出都占一行,100个数字都处理完,程序退出。

让我们的第拾捌个汇编程序来挑战一下——大道至简

functions.asm 略

 1; Fizzbuzz
 2; Compile with: nasm -f elf fizzbuzz.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 fizzbuzz.o -o fizzbuzz
 4; Run with: ./fizzbuzz
 5 
 6%include        'functions.asm'
 7 
 8SECTION .data
 9fizz        db      'Fizz', 0h     ; a message string
10buzz        db      'Buzz', 0h     ; a message string
11 
12SECTION .text
13global  _start
14 
15_start:
16 
17    mov     esi, 0          ; initialise our checkFizz boolean variable
18    mov     edi, 0          ; initialise our checkBuzz boolean variable
19    mov     ecx, 0          ; initialise our counter variable
20 
21nextNumber:
22    inc     ecx             ; increment our counter variable
23 
24.checkFizz
25    mov     edx, 0          ; clear the edx register - this will hold our remainder after division
26    mov     eax, ecx        ; move the value of our counter into eax for division
27    mov     ebx, 3          ; move our number to divide by into ebx (in this case the value is 3)
28    div     ebx             ; divide eax by ebx
29    mov     edi, edx        ; move our remainder into edi (our checkFizz boolean variable)
30    cmp     edi, 0          ; compare if the remainder is zero (meaning the counter divides by 3)
31    jne     .checkBuzz      ; if the remainder is not equal to zero jump to local label checkBuzz
32    mov     eax, fizz       ; else move the address of our fizz string into eax for printing
33    call    sprint          ; call our string printing function
34 
35.checkBuzz:
36    mov     edx, 0          ; clear the edx register - this will hold our remainder after division
37    mov     eax, ecx        ; move the value of our counter into eax for division
38    mov     ebx, 5          ; move our number to divide by into ebx (in this case the value is 5)
39    div     ebx             ; divide eax by ebx
40    mov     esi, edx        ; move our remainder into edi (our checkBuzz boolean variable)
41    cmp     esi, 0          ; compare if the remainder is zero (meaning the counter divides by 5)
42    jne     .checkInt       ; if the remainder is not equal to zero jump to local label checkInt
43    mov     eax, buzz       ; else move the address of our buzz string into eax for printing
44    call    sprint          ; call our string printing function
45 
46.checkInt:
47    cmp     edi, 0          ; edi contains the remainder after the division in checkFizz
48    je     .continue        ; if equal (counter divides by 3) skip printing the integer
49    cmp     esi, 0          ; esi contains the remainder after the division in checkBuzz
50    je     .continue        ; if equal (counter divides by 5) skip printing the integer
51    mov     eax, ecx        ; else move the value in ecx (our counter) into eax for printing
52    call    iprint          ; call our integer printing function
53 
54.continue:
55    mov     eax, 0Ah        ; move an ascii linefeed character into eax
56    push    eax             ; push the address of eax onto the stack for printing
57    mov     eax, esp        ; get the stack pointer (address on the stack of our linefeed char)
58    call    sprint          ; call our string printing function to print a line feed
59    pop     eax             ; pop the stack so we don't waste resources
60    cmp     ecx, 100        ; compare if our counter is equal to 100
61    jne     nextNumber      ; if not equal jump to the start of the loop
62 
63    call    quit            ; else call our quit function
 1$ nasm -f elf fizzbuzz.asm
 2$ ld -m elf_i386 fizzbuzz.o -o fizzbuzz
 3$ ./fizzbuzz
 41
 52
 6Fizz
 74
 8Buzz
 9Fizz
107
118
12Fizz
13Buzz
1411
15Fizz
1613
1714
18FizzBuzz
1916
20.
21.
22.

Lesson 19 执行命令

再一次,背景知识

通过指定的要运行的命令,调用EXEC一族的函数将启动一个新进程来替换当前的。本节,我们将使用sys_execve系统调用,启动 Linux 下的程序/bin/echo,来替换程序运行中的进程,我们让/bin/echo来输出"Hello,World!"

命名约定

这一族的函数都以exec开头,后面跟着的字母含义如下:

  • E,指向环境变量指针的数组,传给进程镜像
  • L,命令行参数分别传给函数
  • P,使用PATH环境变量查找路径参数中的命令名并执行
  • V,命令行参数作为指针数组传递给函数

V 和 E 后缀的函数,意味着需要按如下格式传递参数:

  • 第一个参数是要被执行的命令的字符串,
  • 后面跟着该命令的参数数组,
  • 最后是新进程将用到的环境变量的数组。

当我们调用一个简单的命令时,不将任何特殊的环境变量传递给新进程,而是传递 0h (NULL)。

命令的参数和环境变量都必须用指针的数组来传递。因此,在定义完字符串后,我们定义了一个包含变量名的null终结(0h结尾)的结构体(数组)。这样,就准备好了应传给sys_execve的所有东西。一旦调用成功,新进程将替换我们的进程,输出返回给了终端。

第拾玖个汇编程序——委重投艰

functions.asm 略

 1; Execute
 2; Compile with: nasm -f elf execute.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 execute.o -o execute
 4; Run with: ./execute
 5 
 6%include        'functions.asm'
 7 
 8SECTION .data
 9command         db      '/bin/echo', 0h     ; command to execute
10arg1            db      'Hello World!', 0h
11arguments       dd      command
12                dd      arg1                ; arguments to pass to commandline (in this case just one)
13                dd      0h                  ; end the struct
14environment     dd      0h                  ; arguments to pass as environment variables (inthis case none) end the struct
15 
16SECTION .text
17global  _start
18 
19_start:
20 
21    mov     edx, environment    ; address of environment variables
22    mov     ecx, arguments      ; address of the arguments to pass to the commandline
23    mov     ebx, command        ; address of the file to execute
24    mov     eax, 11             ; invoke SYS_EXECVE (kernel opcode 11)
25    int     80h
26 
27    call    quit                ; call our quit function
1$ nasm -f elf execute.asm
2$ ld -m elf_i386 execute.o -o execute
3$ ./execute
4Hello World!
提示

不妨试试其他命令

1SECTION .data
2command         db      '/bin/ls', 0h       ; command to execute
3arg1            db      '-l', 0h
1SECTION .data
2command         db      '/bin/sleep', 0h    ; command to execute
3arg1            db      '5', 0h

Lesson 20 Process Forking

重入,背景知识

本节引入著名的sys_fork。这个调用不是替换,而是复制我们的进程。该调用不接受任何参数,只要在当前进程中调用,那么新进程就会被创建。新进程与原进程并发执行。

通过检测EAX中的值,来判断当前是处于父进程还是子进程中。父进程返回一个正整数;子进程中EAX为 0。以此可以对于父子进程进行分支逻辑。

基于以上事实,我们在父子进程中打印不同的消息。

注意

每个进程需要各自安全退出

第贰拾个汇编程序——如出一辙

functions.asm 略

 1; Fork
 2; Compile with: nasm -f elf fork.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 fork.o -o fork
 4; Run with: ./fork
 5 
 6%include        'functions.asm'
 7 
 8SECTION .data
 9childMsg        db      'This is the child process', 0h     ; a message string
10parentMsg       db      'This is the parent process', 0h    ; a message string
11 
12SECTION .text
13global  _start
14 
15_start:
16 
17    mov     eax, 2              ; invoke SYS_FORK (kernel opcode 2)
18    int     80h
19 
20    cmp     eax, 0              ; if eax is zero we are in the child process
21    jz      child               ; jump if eax is zero to child label
22 
23parent:
24    mov     eax, parentMsg      ; inside our parent process move parentMsg into eax
25    call    sprintLF            ; call our string printing with linefeed function
26 
27    call    quit                ; quit the parent process
28 
29child:
30    mov     eax, childMsg       ; inside our child process move childMsg into eax
31    call    sprintLF            ; call our string printing with linefeed function
32 
33    call    quit                ; quit the child process
1$ nasm -f elf fork.asm
2$ ld -m elf_i386 fork.o -o fork
3$ ./fork
4This is the parent process
5This is the child process

Lesson 21 输出时间

在 NASM 中,生成一个 unix 时间戳只需简单的向内核调用sys_time,也即:调用表中的 OPCODE 13。不需要参数,返回的UNIX 纪元时间保存在EAX寄存器中

第贰拾壹个汇编程序——只争朝夕

functions.asm 略

 1; Time
 2; Compile with: nasm -f elf time.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 time.o -o time
 4; Run with: ./time
 5 
 6%include        'functions.asm'
 7 
 8SECTION .data
 9msg        db      'Seconds since Jan 01 1970: ', 0h     ; a message string
10 
11SECTION .text
12global  _start
13 
14_start:
15 
16    mov     eax, msg        ; move our message string into eax for printing
17    call    sprint          ; call our string printing function
18 
19    mov     eax, 13         ; invoke SYS_TIME (kernel opcode 13)
20    int     80h             ; call the kernel
21 
22    call    iprintLF        ; call our integer printing function with linefeed
23    call    quit            ; call our quit function
1$ nasm -f elf time.asm
2$ ld -m elf_i386 time.o -o time
3$ ./time
4Seconds since Jan 01 1970: 1374995660

Lesson 22 文件操作——Create

相关背景

文件操作在 Linux 系统中涉及到一小股系统调用,包括:创建、更新、删除。这些函数都必须作用于文件描述符——系统中用于标识文件的一个唯一的、非负整数。

首先登场的是用于创建文件的sys_creat。在后面的课程中,将在同一程序上不断扩展。最终,我们将有一个包含文件的创建、更新、打开、关闭和删除的完整功能的程序。

我们的第贰拾贰个汇编程序——本立道生

functions.asm 略

 1; Create
 2; Compile with: nasm -f elf create.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 create.o -o create
 4; Run with: ./create
 5 
 6%include    'functions.asm'
 7 
 8SECTION .data
 9filename db 'readme.txt',       ; the filename to create
10 
11SECTION .text
12global  _start
13 
14_start:
15 
16    mov     ecx, 0777           ; set all permissions to read, write, execute
17    mov     ebx, filename       ; filename we will create
18    mov     eax, 8              ; invoke SYS_CREAT (kernel opcode 8)
19    int     80h                 ; call the kernel
20 
21    call    quit                ; call our quit function
1$ nasm -f elf create.asm
2$ ld -m elf_i386 create.o -o create
3$ ./create
提示

“没有消息就是好消息”,文件 readme.txt 将出现在与程序相同的目录中


Lesson 23 文件操作——Write

基于上一节的程序,我们调用sys_write将内容写到新创建的文件。

sys_write需要的 3 个参数由以下寄存器提供

  • 要写入的字节数载入EDX
  • 要写入内容的指针载入ECX
  • 文件描述符载入EBX

OPCODE 12 载入EAX,熟悉的套路,INT 80h 开调。留意,我们首先通过将文件名传给sys_creat,进而从EAX中得到文件描述符。

我们的第贰拾叁个汇编程序——握素怀铅

functions.asm 略

 1; Write
 2; Compile with: nasm -f elf write.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 write.o -o write
 4; Run with: ./write
 5 
 6%include    'functions.asm'
 7 
 8SECTION .data
 9filename db 'readme.txt', 0h    ; the filename to create
10contents db 'Hello world!', 0h  ; the contents to write
11 
12SECTION .text
13global  _start
14 
15_start:
16 
17    mov     ecx, 0777           ; code continues from lesson 22
18    mov     ebx, filename
19    mov     eax, 8
20    int     80h
21 
22    mov     edx, 12             ; number of bytes to write - one for each letter of our contents string
23    mov     ecx, contents       ; move the memory address of our contents string into ecx
24    mov     ebx, eax            ; move the file descriptor of the file we created into ebx
25    mov     eax, 4              ; invoke SYS_WRITE (kernel opcode 4)
26    int     80h                 ; call the kernel
27 
28    call    quit                ; call our quit function
1$ nasm -f elf write.asm
2$ ld -m elf_i386 write.o -o write
3$ ./write
提示

执行完打开 readme.txt 看看吧,该有 Hello, World! 才对


Lesson 24 文件操作——Open

继续扩展前面的程序,这次轮到sys_open登场。这个调用用来获取文件描述符,而文件描述符则作为后面其他文件相关函数的参数。

sys_open需要的两个参数:

  • 访问模式(见下表)载入ECX
  • EBX则存着文件名

系统调用的方法我们已经很熟悉了:OPCODE 5 载入EAXINT 80h 开调。

sys_open也可以额外接受 0 个或多个表示文件创建和文件状态的标志,通过EDX读取。详情参见:open(2) —— Linux manual page

名称 描述
O_RDONLY 只读打开 0
O_WRONLY 只写打开 1
O_RDWR 读写打开 2
注意

调用sys_open后,我们从EAX中获得文件描述符,然后使用整数打印函数将这个在 Linux 中唯一的,非负整数的值打印出来

写下我们的第贰拾肆个汇编程序——招之即来

functions.asm 略

 1; Open
 2; Compile with: nasm -f elf open.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 open.o -o open
 4; Run with: ./open
 5 
 6%include    'functions.asm'
 7 
 8SECTION .data
 9filename db 'readme.txt', 0h    ; the filename to create
10contents db 'Hello world!', 0h  ; the contents to write
11 
12SECTION .text
13global  _start
14 
15_start:
16 
17    mov     ecx, 0777           ; Create file from lesson 22
18    mov     ebx, filename
19    mov     eax, 8
20    int     80h
21 
22    mov     edx, 12             ; Write contents to file from lesson 23
23    mov     ecx, contents
24    mov     ebx, eax
25    mov     eax, 4
26    int     80h
27 
28    mov     ecx, 0              ; flag for readonly access mode (O_RDONLY)
29    mov     ebx, filename       ; filename we created above
30    mov     eax, 5              ; invoke SYS_OPEN (kernel opcode 5)
31    int     80h                 ; call the kernel
32 
33    call    iprintLF            ; call our integer printing function
34    call    quit                ; call our quit function
1$ nasm -f elf open.asm
2$ ld -m elf_i386 open.o -o open
3$ ./open
44

Lesson 25 文件操作——Read

介绍了创建、写入、打开,也该读取了。本节我们使用sys_read读取新建且打开的文件,内容存到一个变量中。

sys_read的 3 个参数如下:

  • 要读取的字节数载入EDX
  • 保存内容的变量地址载入ECX
  • EBX中放文件描述符(通过上一节的sys_open获取)

OPCODE 3 载入EAXINT 80h 开调。

第贰拾伍个汇编程序——一览无遗

functions.asm 略

 1; Read
 2; Compile with: nasm -f elf read.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 read.o -o read
 4; Run with: ./read
 5 
 6%include    'functions.asm'
 7 
 8SECTION .data
 9filename db 'readme.txt', 0h    ; the filename to create
10contents db 'Hello world!', 0h  ; the contents to write
11 
12SECTION .bss
13fileContents resb 255,          ; variable to store file contents
14 
15SECTION .text
16global  _start
17 
18_start:
19 
20    mov     ecx, 0777           ; Create file from lesson 22
21    mov     ebx, filename
22    mov     eax, 8
23    int     80h
24 
25    mov     edx, 12             ; Write contents to file from lesson 23
26    mov     ecx, contents
27    mov     ebx, eax
28    mov     eax, 4
29    int     80h
30 
31    mov     ecx, 0              ; Open file from lesson 24
32    mov     ebx, filename
33    mov     eax, 5
34    int     80h
35 
36    mov     edx, 12             ; number of bytes to read - one for each letter of the file contents
37    mov     ecx, fileContents   ; move the memory address of our file contents variable into ecx
38    mov     ebx, eax            ; move the opened file descriptor into EBX
39    mov     eax, 3              ; invoke SYS_READ (kernel opcode 3)
40    int     80h                 ; call the kernel
41 
42    mov     eax, fileContents   ; move the memory address of our file contents variable into eax for printing
43    call    sprintLF            ; call our string printing function
44 
45    call    quit                ; call our quit function
1$ nasm -f elf read.asm
2$ ld -m elf_i386 read.o -o read
3$ ./read
4Hello world!

Lesson 26 文件操作——Close

抱歉这么重要的操作现在才引入,正确的关闭文件等资源无论在那种语言编写的程序中都是不可或缺的步骤,本节引入sys_close

sys_close只需要一个参数:

  • 藉由EBX将文件描述符传给内核

扩展上一节的程序——在获取到文件描述符后,我们将其放入EBX。

OPCODE 6 载入EAXINT 80h 开调。

我们的负责任的第贰拾陆个汇编程序——止戈散马

functions.asm 略

 1; Close
 2; Compile with: nasm -f elf close.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 close.o -o close
 4; Run with: ./close
 5 
 6%include    'functions.asm'
 7 
 8SECTION .data
 9filename db 'readme.txt', 0h    ; the filename to create
10contents db 'Hello world!', 0h  ; the contents to write
11 
12SECTION .bss
13fileContents resb 255,          ; variable to store file contents
14 
15SECTION .text
16global  _start
17 
18_start:
19 
20    mov     ecx, 0777           ; Create file from lesson 22
21    mov     ebx, filename
22    mov     eax, 8
23    int     80h
24 
25    mov     edx, 12             ; Write contents to file from lesson 23
26    mov     ecx, contents
27    mov     ebx, eax
28    mov     eax, 4
29    int     80h
30 
31    mov     ecx, 0              ; Open file from lesson 24
32    mov     ebx, filename
33    mov     eax, 5
34    int     80h
35 
36    mov     edx, 12             ; Read file from lesson 25
37    mov     ecx, fileContents
38    mov     ebx, eax
39    mov     eax, 3
40    int     80h
41 
42    mov     eax, fileContents
43    call    sprintLF
44 
45    mov     ebx, ebx            ; not needed but used to demonstrate that SYS_CLOSE takes a file descriptor from EBX
46    mov     eax, 6              ; invoke SYS_CLOSE (kernel opcode 6)
47    int     80h                 ; call the kernel
48 
49    call    quit                ; call our quit function
1$ nasm -f elf close.asm
2$ ld -m elf_i386 close.o -o close
3$ ./close
4Hello world!
提示

我们正确的关闭了文件,将文件描述符等一干资源还回了操作系统


Lesson 27 文件操作——Seek

之前的课程都是对文件整存整取操作,但实际应用中的更多是对文件的局部修改。本节引入sys_lseek,演示在文件的结尾追加内容。

通过sys_lseek你可以在文件中移动一个叫游标的玩意儿,同时用字节为单位的偏移量来精确定位。下面的例子演示了移动到文件结尾,并用 0 字节偏移量(这样保证我们在末尾写入而不是超出)来定位写入的位置。在ECXEDX中尝试不同的值在文件的不同位置写入。

sys_lseek的3个参数这样传递:

  • EDX保存从哪开始,可选的值如下表
名称 描述
SEEK_SET 文件的开头 0
SEEK_CUR 当前位置偏移量 1
SEEK_END 文件的结尾 2
  • 偏移量存进ECX
  • 最后自然就是文件描述符了,由EBX存储

OPCODE 19 载入EAXINT 80h 开调。定位到期望的位置后,我们调用了sys_write更新文件的内容。

注意

本程序没有创建文件的过程,是在已经存在的 readme.txt 文件上操作的。程序运行后该文件的内容将被更新

有请我们的第贰拾柒个汇编程序——上下求索

functions.asm 略

 1; Seek
 2; Compile with: nasm -f elf seek.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 seek.o -o seek
 4; Run with: ./seek
 5 
 6%include    'functions.asm'
 7 
 8SECTION .data
 9filename db 'readme.txt', 0h    ; the filename to create
10contents  db '-updated-', 0h     ; the contents to write at the start of the file
11 
12SECTION .text
13global  _start
14 
15_start:
16 
17    mov     ecx, 1              ; flag for writeonly access mode (O_WRONLY)
18    mov     ebx, filename       ; filename of the file to open
19    mov     eax, 5              ; invoke SYS_OPEN (kernel opcode 5)
20    int     80h                 ; call the kernel
21 
22    mov     edx, 2              ; whence argument (SEEK_END)
23    mov     ecx, 0              ; move the cursor 0 bytes
24    mov     ebx, eax            ; move the opened file descriptor into EBX
25    mov     eax, 19             ; invoke SYS_LSEEK (kernel opcode 19)
26    int     80h                 ; call the kernel
27 
28    mov     edx, 9              ; number of bytes to write - one for each letter of our contents string
29    mov     ecx, contents       ; move the memory address of our contents string into ecx
30    mov     ebx, ebx            ; move the opened file descriptor into EBX (not required as EBX already has the FD)
31    mov     eax, 4              ; invoke SYS_WRITE (kernel opcode 4)
32    int     80h                 ; call the kernel
33 
34    call    quit                ; call our quit function
1$ nasm -f elf seek.asm
2$ ld -m elf_i386 seek.o -o seek
3$ ./seek

Lesson 28 文件操作——Delete

文件删除在 Linux 中由系统调用sys_unlink(差点写成 unlike)提供。

sys_unlink只需要一个参数:

  • 把文件名放入EBX

OPCODE 10 载入EAXINT 80h 开调。

注意

readme.txt 是已经存在的文件。程序运行完成,该文件将被删除

我们的第贰拾捌个汇编程序——一扫而尽

functions.asm 略

 1; Unlink
 2; Compile with: nasm -f elf unlink.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 unlink.o -o unlink
 4; Run with: ./unlink
 5 
 6%include    'functions.asm'
 7 
 8SECTION .data
 9filename db 'readme.txt', 0h    ; the filename to delete
10 
11SECTION .text
12global  _start
13 
14_start:
15 
16    mov     ebx, filename       ; filename we will delete
17    mov     eax, 10             ; invoke SYS_UNLINK (kernel opcode 10)
18    int     80h                 ; call the kernel
19 
20    call    quit                ; call our quit function
1$ nasm -f elf unlink.asm
2$ ld -m elf_i386 unlink.o -o unlink
3$ ./unlink

Lesson 29 套接字——Create

背景知识,走起

Linux 中的套接字编程,藉由sys_socketcall内核函数提供。与文件操作的一族函数不同,他一人就封装了套接字相关的全部操作,全都以子例程的形式存在其中。我们通过在EBX中传递给他不同的值,来区分诸如:创建、监听、发送、接受、关闭等操作。这里有详细注释的完整程序以供参考。

发起系统调用前,需要初始化一些寄存器,用以存储后面的重要数据。接着调用sys_socketcall的第一个子例程socket来创建套接字。之后的课程中,将在此程序基础上按需扩充。最终,我们将拥有一个包含:创建、绑定、监听、接受、读取、写入和关闭的完整套接字程序。

sys_socketcall的子例程socket接收两个参数:

  • 参数数组的指针由ECX保存
  • 整数 1 由EBX保存

OPCODE 102 载入EAXINT 80h 开调。

在 Linux 中一切皆文件,成功创建后的套接字将作为文件描述符(FD)由EAX返回给用户程序。这个 FD 将用来实施其他的套接字函数。

提示

一个寄存器对自身进行 XOR 异或是初始化(清零)的好办法,这可以确保其中不包含意外的值,从而避免程序崩溃。

套接字,我们的第贰拾玖个汇编程序来了——只如初见

functions.asm 略

 1; Socket
 2; Compile with: nasm -f elf socket.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 socket.o -o socket
 4; Run with: ./socket
 5 
 6%include    'functions.asm'
 7 
 8SECTION .text
 9global  _start
10 
11_start:
12 
13    xor     eax, eax            ; init eax 0
14    xor     ebx, ebx            ; init ebx 0
15    xor     edi, edi            ; init edi 0
16    xor     esi, esi            ; init esi 0
17 
18_socket:
19 
20    push    byte 6              ; push 6 onto the stack (IPPROTO_TCP)
21    push    byte 1              ; push 1 onto the stack (SOCK_STREAM)
22    push    byte 2              ; push 2 onto the stack (PF_INET)
23    mov     ecx, esp            ; move address of arguments into ecx
24    mov     ebx, 1              ; invoke subroutine SOCKET (1)
25    mov     eax, 102            ; invoke SYS_SOCKETCALL (kernel opcode 102)
26    int     80h                 ; call the kernel
27 
28    call    iprintLF            ; call our integer printing function (print the file descriptor in EAX or -1 on error)
29 
30_exit:
31 
32    call    quit                ; call our quit function
1$ nasm -f elf socket.asm
2$ ld -m elf_i386 socket.o -o socket
3$ ./socket
43

Lesson 30 套接字——Bind

上一节完成了套接字的创建,现在我们为这个套接字关联一个本地 IP 和端口以便其他程序与其建立连接。这个任务由sys_cocketcall的第二个子例程bind完成。

这里引入新的寄存器EDI来存储套接字的文件描述符(FD)。EDI——目标索引寄存器,原做拷贝过程中存储目标文件的位置之用。

sys_socketcall的子例程bind也接收两个参数:

  • 参数数组的指针由ECX保存
  • 整数 2 由EBX保存

OPCODE 102 载入EAXINT 80h 开调。

第叁拾个汇编程序——一朝比翼

functions.asm 略

 1; Socket
 2; Compile with: nasm -f elf socket.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 socket.o -o socket
 4; Run with: ./socket
 5 
 6%include    'functions.asm'
 7 
 8SECTION .text
 9global  _start
10 
11_start:
12 
13    xor     eax, eax            ; initialize some registers
14    xor     ebx, ebx
15    xor     edi, edi
16    xor     esi, esi
17 
18_socket:
19 
20    push    byte 6              ; create socket from lesson 29
21    push    byte 1
22    push    byte 2
23    mov     ecx, esp
24    mov     ebx, 1
25    mov     eax, 102
26    int     80h
27 
28_bind:
29 
30    mov     edi, eax            ; move return value of SYS_SOCKETCALL into edi (file descriptor for new socket, or -1 on error)
31    push    dword 0x00000000    ; push 0 dec onto the stack IP ADDRESS (0.0.0.0)
32    push    word 0x2923         ; push 9001 dec onto stack PORT (reverse byte order)
33    push    word 2              ; push 2 dec onto stack AF_INET
34    mov     ecx, esp            ; move address of stack pointer into ecx
35    push    byte 16             ; push 16 dec onto stack (arguments length)
36    push    ecx                 ; push the address of arguments onto stack
37    push    edi                 ; push the file descriptor onto stack
38    mov     ecx, esp            ; move address of arguments into ecx
39    mov     ebx, 2              ; invoke subroutine BIND (2)
40    mov     eax, 102            ; invoke SYS_SOCKETCALL (kernel opcode 102)
41    int     80h                 ; call the kernel
42 
43_exit:
44 
45    call    quit                ; call our quit function
1$ nasm -f elf socket.asm
2$ ld -m elf_i386 socket.o -o socket
3$ ./socket

Lesson 31 套接字——Listen

上一节完成了套接字的绑定。下面由sys_socketcalllisten子例程告诉我们的套接字监听TCP的入站请求。这是在套接字上对与我们相互连接的程序进行读写的前提。

sys_socketcall的子例程listen也接收两个参数:

  • 参数数组的指针由ECX保存
  • 整数 4 由EBX保存

OPCODE 102 载入EAXINT 80h 开调。调用成功后,套接字即开始监听入站请求。

我们的第叁拾壹个汇编程序——伫候佳音

functions.asm 略

 1; Socket
 2; Compile with: nasm -f elf socket.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 socket.o -o socket
 4; Run with: ./socket
 5 
 6%include    'functions.asm'
 7 
 8SECTION .text
 9global  _start
10 
11_start:
12 
13    xor     eax, eax            ; initialize some registers
14    xor     ebx, ebx
15    xor     edi, edi
16    xor     esi, esi
17 
18_socket:
19 
20    push    byte 6              ; create socket from lesson 29
21    push    byte 1
22    push    byte 2
23    mov     ecx, esp
24    mov     ebx, 1
25    mov     eax, 102
26    int     80h
27 
28_bind:
29 
30    mov     edi, eax            ; bind socket from lesson 30
31    push    dword 0x00000000
32    push    word 0x2923
33    push    word 2
34    mov     ecx, esp
35    push    byte 16
36    push    ecx
37    push    edi
38    mov     ecx, esp
39    mov     ebx, 2
40    mov     eax, 102
41    int     80h
42 
43_listen:
44 
45    push    byte 1              ; move 1 onto stack (max queue length argument)
46    push    edi                 ; push the file descriptor onto stack
47    mov     ecx, esp            ; move address of arguments into ecx
48    mov     ebx, 4              ; invoke subroutine LISTEN (4)
49    mov     eax, 102            ; invoke SYS_SOCKETCALL (kernel opcode 102)
50    int     80h                 ; call the kernel
51 
52_exit:
53 
54    call    quit                ; call our quit function
1$ nasm -f elf socket.asm
2$ ld -m elf_i386 socket.o -o socket
3$ ./socket

Lesson 32 套接字——Accept

前面的课程都是套接字的准备工作。入站请求到达后,还必须有接受逻辑,这由sys_socketcallaccept子例程实现。进入 ACCEPT 状态的套接字可以在远程连接上进行读写。

sys_socketcall的子例程accept也接收两个参数:

  • 参数数组的指针由ECX保存
  • 整数 5 由EBX保存

OPCODE 102 载入EAXINT 80h 开调。accept子例程将创建另一个文件描述符(FD),用以标识入站连接。我们后面的课程将使用这个 FD 来进行实际的读写操作。

提示

运行本程序后,开启另一个终端窗口,键入sudo netstat -plnt查看 9001 端口是否在监听之中

第叁拾贰个汇编程序——宾至如归

functions.asm 略

 1; Socket
 2; Compile with: nasm -f elf socket.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 socket.o -o socket
 4; Run with: ./socket
 5 
 6%include    'functions.asm'
 7 
 8SECTION .text
 9global  _start
10 
11_start:
12 
13    xor     eax, eax            ; initialize some registers
14    xor     ebx, ebx
15    xor     edi, edi
16    xor     esi, esi
17 
18_socket:
19 
20    push    byte 6              ; create socket from lesson 29
21    push    byte 1
22    push    byte 2
23    mov     ecx, esp
24    mov     ebx, 1
25    mov     eax, 102
26    int     80h
27 
28_bind:
29 
30    mov     edi, eax            ; bind socket from lesson 30
31    push    dword 0x00000000
32    push    word 0x2923
33    push    word 2
34    mov     ecx, esp
35    push    byte 16
36    push    ecx
37    push    edi
38    mov     ecx, esp
39    mov     ebx, 2
40    mov     eax, 102
41    int     80h
42 
43_listen:
44 
45    push    byte 1              ; listen socket from lesson 31
46    push    edi
47    mov     ecx, esp
48    mov     ebx, 4
49    mov     eax, 102
50    int     80h
51 
52_accept:
53 
54    push    byte 0              ; push 0 dec onto stack (address length argument)
55    push    byte 0              ; push 0 dec onto stack (address argument)
56    push    edi                 ; push the file descriptor onto stack
57    mov     ecx, esp            ; move address of arguments into ecx
58    mov     ebx, 5              ; invoke subroutine ACCEPT (5)
59    mov     eax, 102            ; invoke SYS_SOCKETCALL (kernel opcode 102)
60    int     80h                 ; call the kernel
61 
62_exit:
63 
64    call    quit                ; call our quit function
1$ nasm -f elf socket.asm
2$ ld -m elf_i386 socket.o -o socket
3$ ./socket

Lesson 33 套接字——Read

套接字也建立了,地址和端口也绑定了,监听中的套接字接受了入站连接,现在该让我们看看远端发来的请求里有什么了吧……

当入站连接被套接字接受后,一个新的文件描述符(FD)通过EAX返回给用户程序。本节将使用这个 FD 从连接中读取请求头。

先将获取到的 FD 存入ESI寄存器——原址索引寄存器,原做拷贝过程中存储来源文件的位置之用。

sys_read是我们的老朋友了,就靠它从套接字连接中读取数据。正如前面的课程中所做的,我们用一个变量来保存从 FD 中读取的内容。本例的套接字使用传说中的HTTP协议进行通讯。分析HTTP请求头部,进而确认入站消息的长度、客户端接收的响应格式等内容超出了本教程的范畴。为了简化,只读取前 255 个字节并打印到标准输出。

一旦入站连接被接受,Web 服务器通常会生成一个子进程来接管读/写通信。父进程得以抽身继续监听和接收新的请求。我们就用前不久学过的sys_fock系统调用和JMP汇编指令来实现这一模式。

使用 curl 工具作为客户端来帮我们生成有效的请求头,并连接到套接字。当然你也可以使用浏览器。

注意

我们在 .bss 段预留 255 字节空间用以存储从 FD 中读取到的内容。.bss 段的信息可以回顾 Lesson 9

提示

程序运行起来后,在另一个终端窗口的命令行输入curl http://localhost:9001来查看程序读取到的请求头

我们的第叁拾叁个汇编程序——目营心匠

functions.asm 略

 1; Socket
 2; Compile with: nasm -f elf socket.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 socket.o -o socket
 4; Run with: ./socket
 5 
 6%include    'functions.asm'
 7 
 8SECTION .bss
 9buffer resb 255,                ; variable to store request headers
10 
11SECTION .text
12global  _start
13 
14_start:
15 
16    xor     eax, eax            ; initialize some registers
17    xor     ebx, ebx
18    xor     edi, edi
19    xor     esi, esi
20 
21_socket:
22 
23    push    byte 6              ; create socket from lesson 29
24    push    byte 1
25    push    byte 2
26    mov     ecx, esp
27    mov     ebx, 1
28    mov     eax, 102
29    int     80h
30 
31_bind:
32 
33    mov     edi, eax            ; bind socket from lesson 30
34    push    dword 0x00000000
35    push    word 0x2923
36    push    word 2
37    mov     ecx, esp
38    push    byte 16
39    push    ecx
40    push    edi
41    mov     ecx, esp
42    mov     ebx, 2
43    mov     eax, 102
44    int     80h
45 
46_listen:
47 
48    push    byte 1              ; listen socket from lesson 31
49    push    edi
50    mov     ecx, esp
51    mov     ebx, 4
52    mov     eax, 102
53    int     80h
54 
55_accept:
56 
57    push    byte 0              ; accept socket from lesson 32
58    push    byte 0
59    push    edi
60    mov     ecx, esp
61    mov     ebx, 5
62    mov     eax, 102
63    int     80h
64 
65_fork:
66 
67    mov     esi, eax            ; move return value of SYS_SOCKETCALL into esi (file descriptor for accepted socket, or -1 on error)
68    mov     eax, 2              ; invoke SYS_FORK (kernel opcode 2)
69    int     80h                 ; call the kernel
70 
71    cmp     eax, 0              ; if return value of SYS_FORK in eax is zero we are in the child process
72    jz      _read               ; jmp in child process to _read
73 
74    jmp     _accept             ; jmp in parent process to _accept
75 
76_read:
77 
78    mov     edx, 255            ; number of bytes to read (we will only read the first 255 bytes for simplicity)
79    mov     ecx, buffer         ; move the memory address of our buffer variable into ecx
80    mov     ebx, esi            ; move esi into ebx (accepted socket file descriptor)
81    mov     eax, 3              ; invoke SYS_READ (kernel opcode 3)
82    int     80h                 ; call the kernel
83 
84    mov     eax, buffer         ; move the memory address of our buffer variable into eax for printing
85    call    sprintLF            ; call our string printing function
86 
87_exit:
88 
89    call    quit                ; call our quit function
1$ nasm -f elf socket.asm
2$ ld -m elf_i386 socket.o -o socket
3$ ./socket
4GET / HTTP/1.1
5Host: localhost:9001
6User-Agent: curl/x.xx.x
7Accept: */*

Lesson 34 套接字——Write

上一节介绍完在套接字上读取,本节说说向套接字写入。

得益于 Linux 中的一切皆文件,我们可以使用已经掌握的sys_write向套接字连接写入数据。程序依然使用HTTP协议。这次作为服务端的我们的程序没人帮忙了,必须要自己构造规定的响应头给客户端程序。格式遵循 RFC 标准

注意

对于已知的值,使用 .data 段来存储。回顾 Lesson 1 关于 .data 段的信息

提示

运行程序,在另一个终端窗口中输入命令curl http://localhost:9001查看响应;或者使用浏览器访问上述地址

神功将成的第叁拾肆个汇编程序——其应若响

functions.asm 略

  1; Socket
  2; Compile with: nasm -f elf socket.asm
  3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 socket.o -o socket
  4; Run with: ./socket
  5 
  6%include    'functions.asm'
  7 
  8SECTION .data
  9; our response string
 10response db 'HTTP/1.1 200 OK', 0Dh, 0Ah, 'Content-Type: text/html', 0Dh, 0Ah, 'Content-Length: 14', 0Dh, 0Ah, 0Dh, 0Ah, 'Hello World!', 0Dh, 0Ah, 0h
 11 
 12SECTION .bss
 13buffer resb 255,                ; variable to store request headers
 14 
 15SECTION .text
 16global  _start
 17 
 18_start:
 19 
 20    xor     eax, eax            ; initialize some registers
 21    xor     ebx, ebx
 22    xor     edi, edi
 23    xor     esi, esi
 24 
 25_socket:
 26 
 27    push    byte 6              ; create socket from lesson 29
 28    push    byte 1
 29    push    byte 2
 30    mov     ecx, esp
 31    mov     ebx, 1
 32    mov     eax, 102
 33    int     80h
 34 
 35_bind:
 36 
 37    mov     edi, eax            ; bind socket from lesson 30
 38    push    dword 0x00000000
 39    push    word 0x2923
 40    push    word 2
 41    mov     ecx, esp
 42    push    byte 16
 43    push    ecx
 44    push    edi
 45    mov     ecx, esp
 46    mov     ebx, 2
 47    mov     eax, 102
 48    int     80h
 49 
 50_listen:
 51 
 52    push    byte 1              ; listen socket from lesson 31
 53    push    edi
 54    mov     ecx, esp
 55    mov     ebx, 4
 56    mov     eax, 102
 57    int     80h
 58 
 59_accept:
 60 
 61    push    byte 0              ; accept socket from lesson 32
 62    push    byte 0
 63    push    edi
 64    mov     ecx, esp
 65    mov     ebx, 5
 66    mov     eax, 102
 67    int     80h
 68 
 69_fork:
 70 
 71    mov     esi, eax            ; fork socket from lesson 33
 72    mov     eax, 2
 73    int     80h
 74 
 75    cmp     eax, 0
 76    jz      _read
 77 
 78    jmp     _accept
 79 
 80_read:
 81 
 82    mov     edx, 255            ; read socket from lesson 33
 83    mov     ecx, buffer
 84    mov     ebx, esi
 85    mov     eax, 3
 86    int     80h
 87 
 88    mov     eax, buffer
 89    call    sprintLF
 90 
 91_write:
 92 
 93    mov     edx, 78             ; move 78 dec into edx (length in bytes to write)
 94    mov     ecx, response       ; move address of our response variable into ecx
 95    mov     ebx, esi            ; move file descriptor into ebx (accepted socket id)
 96    mov     eax, 4              ; invoke SYS_WRITE (kernel opcode 4)
 97    int     80h                 ; call the kernel
 98 
 99_exit:
100 
101    call    quit                ; call our quit function
1$ nasm -f elf socket.asm
2$ ld -m elf_i386 socket.o -o socket
3$ ./socket

再开一个终端窗口

1$ curl http://localhost:9001
2Hello World!

Lesson 35 套接字——Close

又一次,来到释放资源的关键步骤!正确的关闭链接和关闭文件一样重要。在程序将响应返回给客户端后,我们要关闭子进程中的活动套接字。归还的资源用来接收新到来的链接。

sys_close不多说,直接秀码——

提示

程序启动后,同样新开一个终端窗口查看响应

我们的第叁拾伍个汇编程序——功成身退

functions.asm 略

  1; Socket
  2; Compile with: nasm -f elf socket.asm
  3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 socket.o -o socket
  4; Run with: ./socket
  5 
  6%include    'functions.asm'
  7 
  8SECTION .data
  9; our response string
 10response db 'HTTP/1.1 200 OK', 0Dh, 0Ah, 'Content-Type: text/html', 0Dh, 0Ah, 'Content-Length: 14', 0Dh, 0Ah, 0Dh, 0Ah, 'Hello World!', 0Dh, 0Ah, 0h
 11 
 12SECTION .bss
 13buffer resb 255,                ; variable to store request headers
 14 
 15SECTION .text
 16global  _start
 17 
 18_start:
 19 
 20    xor     eax, eax            ; initialize some registers
 21    xor     ebx, ebx
 22    xor     edi, edi
 23    xor     esi, esi
 24 
 25_socket:
 26 
 27    push    byte 6              ; create socket from lesson 29
 28    push    byte 1
 29    push    byte 2
 30    mov     ecx, esp
 31    mov     ebx, 1
 32    mov     eax, 102
 33    int     80h
 34 
 35_bind:
 36 
 37    mov     edi, eax            ; bind socket from lesson 30
 38    push    dword 0x00000000
 39    push    word 0x2923
 40    push    word 2
 41    mov     ecx, esp
 42    push    byte 16
 43    push    ecx
 44    push    edi
 45    mov     ecx, esp
 46    mov     ebx, 2
 47    mov     eax, 102
 48    int     80h
 49 
 50_listen:
 51 
 52    push    byte 1              ; listen socket from lesson 31
 53    push    edi
 54    mov     ecx, esp
 55    mov     ebx, 4
 56    mov     eax, 102
 57    int     80h
 58 
 59_accept:
 60 
 61    push    byte 0              ; accept socket from lesson 32
 62    push    byte 0
 63    push    edi
 64    mov     ecx, esp
 65    mov     ebx, 5
 66    mov     eax, 102
 67    int     80h
 68 
 69_fork:
 70 
 71    mov     esi, eax            ; fork socket from lesson 33
 72    mov     eax, 2
 73    int     80h
 74 
 75    cmp     eax, 0
 76    jz      _read
 77 
 78    jmp     _accept
 79 
 80_read:
 81 
 82    mov     edx, 255            ; read socket from lesson 33
 83    mov     ecx, buffer
 84    mov     ebx, esi
 85    mov     eax, 3
 86    int     80h
 87 
 88    mov     eax, buffer
 89    call    sprintLF
 90 
 91_write:
 92 
 93    mov     edx, 78             ; write socket from lesson 34
 94    mov     ecx, response
 95    mov     ebx, esi
 96    mov     eax, 4
 97    int     80h
 98 
 99_close:
100 
101    mov     ebx, esi            ; move esi into ebx (accepted socket file descriptor)
102    mov     eax, 6              ; invoke SYS_CLOSE (kernel opcode 6)
103    int     80h                 ; call the kernel
104 
105_exit:
106 
107    call    quit                ; call our quit function
1$ nasm -f elf socket.asm
2$ ld -m elf_i386 socket.o -o socket
3$ ./socket

同样,再另一个终端窗口中

1$ curl http://localhost:9001
2Hello World!

Lesson 36 套接字——下载网页

前面的课程中,我们见识了sys_socketcall的众多子例程是如何创建、管理 Linux 套接字并在其中传输数据的,主要集中在服务端程序。接下来,从客户端的角度,使用connect子例程来演示——从远端下载网页。

步骤如下:

  • 首先调用sys_socketcallsocket子例程创建套接字,用来将请求发送到远端
  • 然后调用sys_socketcallconnect子例程将刚创建的套接字连到远程 Web 服务器
  • 之后调用sys_write发送HTTP格式的请求
  • 接着调用sys_read接收来自 Web 服务器的HTTP格式响应

服务器返回的响应内容,自然由我们的字符串打印函数代劳输出到终端上。

简单介绍下 HTTP 请求

HTTP 规范涉及多个版本的标准:1.0 in RFC19451.1 in RFC2068 以及 2.0 in RFC7540。1.1 版本时至今日依然是主流。

一个 HTTP 请求包含 3 个部分:

  1. 一个包含请求方法请求 URL协议版本的行
  2. 可选的请求头部分
  3. 一个空行,用以告知服务器请求方以完成请求并等待服务器响应

一个典型的到根文档的 HTTP 请求一般长这样:

1GET / HTTP/1.1                  ; A line containing the request method, url and version
2Host: asmtutor.com              ; A section of request headers
3                                ; A required empty line

本节的程序与上一节的程序拥有相似的开头部分,但作为客户端的我们,不需要调用bind,取而代之的是调用connect,连接到指定 IP 地址和端口的远程 Web 服务器。然后使用sys_writesys_read在两个套接字之间通过HTTP请求HTTP响应来传输数据。

sys_socketcall的子例程connect接收两个参数:

  • 参数数组的指针由ECX保存
  • 整数 3 由EBX保存

OPCODE 102 载入EAXINT 80h 开调。

提示

提示:为避免返回的内容过多而充斥屏幕,可在运行程序时使用输出重定向./crawler > index.html来保存服务器响应到一个文件,而不是直接输出到终端上。

我们的集大成的第叁拾陆个汇编程序——百川朝海

functions.asm 略

 1; Crawler
 2; Compile with: nasm -f elf crawler.asm
 3; Link with (64 bit systems require elf_i386 option): ld -m elf_i386 crawler.o -o crawler
 4; Run with: ./crawler
 5 
 6%include    'functions.asm'
 7 
 8SECTION .data
 9; our request string
10request db 'GET / HTTP/1.1', 0Dh, 0Ah, 'Host: 139.162.39.66:80', 0Dh, 0Ah, 0Dh, 0Ah, 0h
11 
12SECTION .bss
13buffer resb 1,                  ; variable to store response
14 
15SECTION .text
16global  _start
17 
18_start:
19 
20    xor     eax, eax            ; init eax 0
21    xor     ebx, ebx            ; init ebx 0
22    xor     edi, edi            ; init edi 0
23 
24_socket:
25 
26    push    byte 6              ; push 6 onto the stack (IPPROTO_TCP)
27    push    byte 1              ; push 1 onto the stack (SOCK_STREAM)
28    push    byte 2              ; push 2 onto the stack (PF_INET)
29    mov     ecx, esp            ; move address of arguments into ecx
30    mov     ebx, 1              ; invoke subroutine SOCKET (1)
31    mov     eax, 102            ; invoke SYS_SOCKETCALL (kernel opcode 102)
32    int     80h                 ; call the kernel
33 
34_connect:
35 
36    mov     edi, eax            ; move return value of SYS_SOCKETCALL into edi (file descriptor for new socket, or -1 on error)
37    push    dword 0x4227a28b    ; push 139.162.39.66 onto the stack IP ADDRESS (reverse byte order)
38    push    word 0x5000         ; push 80 onto stack PORT (reverse byte order)
39    push    word 2              ; push 2 dec onto stack AF_INET
40    mov     ecx, esp            ; move address of stack pointer into ecx
41    push    byte 16             ; push 16 dec onto stack (arguments length)
42    push    ecx                 ; push the address of arguments onto stack
43    push    edi                 ; push the file descriptor onto stack
44    mov     ecx, esp            ; move address of arguments into ecx
45    mov     ebx, 3              ; invoke subroutine CONNECT (3)
46    mov     eax, 102            ; invoke SYS_SOCKETCALL (kernel opcode 102)
47    int     80h                 ; call the kernel
48 
49_write:
50 
51    mov     edx, 43             ; move 43 dec into edx (length in bytes to write)
52    mov     ecx, request        ; move address of our request variable into ecx
53    mov     ebx, edi            ; move file descriptor into ebx (created socket file descriptor)
54    mov     eax, 4              ; invoke SYS_WRITE (kernel opcode 4)
55    int     80h                 ; call the kernel
56 
57_read:
58 
59    mov     edx, 1              ; number of bytes to read (we will read 1 byte at a time)
60    mov     ecx, buffer         ; move the memory address of our buffer variable into ecx
61    mov     ebx, edi            ; move edi into ebx (created socket file descriptor)
62    mov     eax, 3              ; invoke SYS_READ (kernel opcode 3)
63    int     80h                 ; call the kernel
64 
65    cmp     eax, 0              ; if return value of SYS_READ in eax is zero, we have reached the end of the file
66    jz      _close              ; jmp to _close if we have reached the end of the file (zero flag set)
67 
68    mov     eax, buffer         ; move the memory address of our buffer variable into eax for printing
69    call    sprint              ; call our string printing function
70    jmp     _read               ; jmp to _read
71 
72_close:
73 
74    mov     ebx, edi            ; move edi into ebx (connected socket file descriptor)
75    mov     eax, 6              ; invoke SYS_CLOSE (kernel opcode 6)
76    int     80h                 ; call the kernel
77 
78_exit:
79 
80    call    quit                ; call our quit function
 1$ nasm -f elf crawler.asm
 2$ ld -m elf_i386 crawler.o -o crawler
 3$ ./crawler
 4HTTP/1.1 200 OK
 5Content-Type: text/html
 6 
 7<!DOCTYPE html>
 8<html lang="en">
 9...
10</html>

$$ The\ End. $$


本文采用 知识共享署名许可协议(CC-BY 4.0)进行许可,转载注明来源即可。如有错误劳烦评论或邮件指出。


comments powered by Disqus