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
将导致我们的代码中到处是非必要的标签。而使用CALL
和RET
,汇编语言将采用称作堆栈的机制代为处理这些细节。
引入栈
栈也在内存中,然而某个程序的栈内存对其而言具备一些特殊性质。栈内存的存取遵循后进先出 Last In First Out memory (LIFO)原则。可以将其想象成厨房中的一摞碟子,最后一个放在顶上的碟子也正是下一次用碟子时第一个被取走的。
诚然,汇编中的栈内存放不下碟子,但是能放二进制数据。变量、地址、甚至其他程序都可以放进去。当调用子例程时,我们需要使用栈来临时存放上述数据以便子例程使用。
通常,执行中的一段代码所使用着的任何寄存器,都应该在调用子例程之前,使用PUSH
指令将其中的数据压入栈,以此确保子例程返回后可以还原这些寄存器的原有值(因为子例程有可能会使用上述寄存器存储新的数据,不预先保存的话被覆盖后就丢失了),还原通过与PUSH
指令执行顺序相反的顺序执行POP
指令来完成。如此,就不必担心子例程执行过程中对上述寄存的修改。
CALL
和RET
两个指令与PUSH
和POP
相似,也使用到了堆栈,但他们除了压栈/弹栈外还做了额外的工作,当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
被打印了两次这一现象,实际上程序并没有逻辑错误,她忠实的履行了职责,完成了我们的任务委托,也即:上一节的代码写法,输出就应该是那样子的。解释现象之前,先分别注释掉打印msg1
或msg2
的代码,只留其中一个看看效果
如果,只注释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 后逐个转换
这里引入两个新的子例程,iprint
和iprintLF
。这些函数用来打印数字的字符串形式,数值本身使用EAX
装载,ECX
用来计数。然后重复除以 10 的过程,每次把余数加 48,结果值PUSH
到栈上以备后用。当除以 10 的商(存在EAX
里)为 0 时,将退出当前循环,进入另一个循环。在该循环里我们通过逐个POP
弹栈的方法,打印每一位数字的字符串形式。弹到什么时候为止呢?这正是我们在ECX
寄存器里存计数的目的,每弹一个值ECX
就减 1,直到减到 0。所有的这些都完成后,程序退出。
除法指令简介
DIV
和IDIV
指令将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 计算——加法
这次的程序,将寄存器EAX
和EBX
中的值相加,和保存在EAX
中。首先MOV
其中一个加数到EAX
(本例中为 90),然后MOV
另一个加数到EBX
(本例中为9)。我们需要调用ADD
指令来实现加法运算。EBX
和EAX
中的值将被加到一起,而结果和将被存回指令最左边的寄存器中(也就是本例的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 码的转换。本例中,依然使用EAX
和EBX
。他们分别保存被除数和除数。如前所述,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
则包含BL
和BH
(低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——老外在学校里教孩子除法的一组游戏。玩法:
- 玩家轮流计算从
1
到100
的整数, - 遇到
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 载入EAX
,INT 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 载入EAX
,INT 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 载入EAX
,INT 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 字节偏移量(这样保证我们在末尾写入而不是超出)来定位写入的位置。在ECX
和EDX
中尝试不同的值在文件的不同位置写入。
sys_lseek
的3个参数这样传递:
EDX
保存从哪开始,可选的值如下表
名称 | 描述 | 值 |
---|---|---|
SEEK_SET | 文件的开头 | 0 |
SEEK_CUR | 当前位置偏移量 | 1 |
SEEK_END | 文件的结尾 | 2 |
- 偏移量存进
ECX
- 最后自然就是文件描述符了,由
EBX
存储
OPCODE 19 载入EAX
,INT 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 载入EAX
,INT 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 载入EAX
,INT 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 载入EAX
,INT 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_socketcall
的listen
子例程告诉我们的套接字监听TCP
的入站请求。这是在套接字上对与我们相互连接的程序进行读写的前提。
sys_socketcall
的子例程listen
也接收两个参数:
- 参数数组的指针由
ECX
保存 - 整数 4 由
EBX
保存
OPCODE 102 载入EAX
,INT 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_socketcall
的accept
子例程实现。进入 ACCEPT 状态的套接字可以在远程连接上进行读写。
sys_socketcall
的子例程accept
也接收两个参数:
- 参数数组的指针由
ECX
保存 - 整数 5 由
EBX
保存
OPCODE 102 载入EAX
,INT 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_socketcall
的socket
子例程创建套接字,用来将请求发送到远端 - 然后调用
sys_socketcall
的connect
子例程将刚创建的套接字连到远程 Web 服务器 - 之后调用
sys_write
发送HTTP
格式的请求 - 接着调用
sys_read
接收来自 Web 服务器的HTTP
格式响应
服务器返回的响应内容,自然由我们的字符串打印函数代劳输出到终端上。
简单介绍下 HTTP 请求
HTTP 规范涉及多个版本的标准:1.0 in RFC1945,1.1 in RFC2068 以及 2.0 in RFC7540。1.1 版本时至今日依然是主流。
一个 HTTP 请求包含 3 个部分:
- 一个包含
请求方法
、请求 URL
和协议版本
的行 - 可选的请求头部分
- 一个空行,用以告知服务器请求方以完成请求并等待服务器响应
一个典型的到根文档的 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_write
和sys_read
在两个套接字之间通过HTTP请求
和HTTP响应
来传输数据。
sys_socketcall
的子例程connect
接收两个参数:
- 参数数组的指针由
ECX
保存 - 整数 3 由
EBX
保存
OPCODE 102 载入EAX
,INT 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)进行许可,转载注明来源即可。如有错误劳烦评论或邮件指出。