如何用NASM汇编写一个静态资源服务器?

328 阅读6分钟

前言

先看一下整体代码,以下是32位模式的。

SECTION .data

headers db 'HTTP/1.1 200 OK', 0Dh, 0Ah, 'Content-Type: application/octet-stream', 0Dh, 0Ah, 0Dh, 0Ah

root db '/home/HouXinLin/test', 0h



SECTION .bss

fileContents resb 40960

responseBuffer resb 40960

requestBuffer resb 4096

fullPath resb 1024

requestPath resb 1024

SECTION .text

global _start

_start:

mov ebp, esp

socket:

push byte 6

push byte 1

push byte 2

mov ecx, esp

mov ebx, 1

mov eax, 102

int 80h ;;创建Socket

bind:

mov edi, eax

push dword 0x00000000

push word 0x901f ;;端口8080

push word 2

mov ecx, esp

push byte 16

push ecx

push edi

mov ecx, esp

mov ebx, 2

mov eax, 102

int 80h ;;绑定8080端口

listen:

push byte 1

push edi

mov ecx, esp

mov ebx, 4

mov eax, 102

int 80h ;监听

accept:

push byte 0

push byte 0

push edi

mov ecx, esp

mov ebx, 5

mov eax, 102

int 80h

mov esi, eax ;;将客户端描述符保存到esi中




read:

mov edx, 4096 ;;读取客户端内容

mov ecx, requestBuffer

mov ebx, esi

mov eax, 3

int 80h




getRequestResourcePath: ;;获取请求资源路径

mov eax,requestBuffer

mov ebx,eax

nextResourceChar:

cmp byte[ebx],32 ;;如果是空格

jz record ;;开始记录

inc ebx ;;下一个字符

jmp nextResourceChar

ret

record:

mov edx,ebx ;;ebx是第一个空格后的位置

sub edx,eax ;存放开始索引

mov ecx, requestBuffer

add ecx,edx ;;从ecx后的位置开始查看地一个空格

mov edx,0

hasEnd:

inc ecx

cmp byte[ecx],32 ;;如果下一个也是空格

jz finishSearch

mov eax, dword[ecx] ;;获取当前字符

mov dword[requestPath+edx],eax

inc edx

jmp hasEnd

finishSearch:

push esi

mov esi,root

mov edi,fullPath

mov ecx,20

rep movsb ;;复制root

mov esi,requestPath

mov edi,fullPath

add edi,20

mov ecx,edx

rep movsb

pop esi

write:

push esi




mov edi,responseBuffer ;;字符复制目的地址

mov esi,headers ;;字符复制原地址

mov ecx,59 ;;复制59个字节到响应buffer中

rep movsb

call readFile ;;读取文件

mov edi,responseBuffer+59 ;;偏移59个字节拼接body

mov esi,fileContents




mov ecx,eax

rep movsb ;;在复制n个字节,eax是读取到的字节数量,不固定

pop esi



mov edx, eax ;;文件内容长度

add edx,59 ;;加上头部长度

mov ecx, responseBuffer ;;输出

mov ebx, esi

mov eax, 4

int 80h

call exit

readFile:

mov ecx, 4 ;;打开文件

mov ebx, fullPath

mov eax, 5

int 80h

mov edx, 40960 ;;尝试读取4096个字节到fileContents

mov ecx, fileContents

mov ebx, eax

mov eax, 3

int 80h ;读取

ret

exit:

mov ebx,0

mov eax,1

int 80h

要了解这段代码,首先还需要了解一下几个知识点,我们从最上层开始看。

首先是HTTP请求报文,其他我们不用解析,只解析请求首行即可,他是这种形式的。

GET /test.txt HTTP/1.1

中间部分是我们需要拿到的,表示客户端想要获取的资源路径。

第二个了解的是socket,以及在linux下创建socket的流程以及读写数据。

第三个了解的是系统调用,socket服务是操作系统提供我们的,我们不能凭空构造一个TCP链接,需要操作系统帮助,所以我们要进行系统调用,触发的时机就是int 80h,这条指令执行后所有操作会转移到内核中的系统调用处理程序,内核处理完后会执行int 80h下面的语句。

但这是不是有个问题,操作系统提供了大量的功能,在int 80h后,操作系统怎么知道你要调用什么服务?

答案就是eax寄存器,系统为每一个功能都编了号,比如输出的调用是4,具体对照表可以参考下面的网址

syscalls32.paolostivanin.com/

这样在int 80h后,操作系统就可以根据eax寄存器的值,处理不同的逻辑,但是还有一个问题,那就是参数,比如当调用输出的时候,你还要告诉系统输出的地址,以及输出多少字节。

而这些参数信息会依次按照顺序保存在ebx、ecx、edx、edx、esi、edi中。

这三个是最基本的。

接下来就是按照NASM的语法,创建socket,解析请求行、读取文件、写回socket了。

我们看前几行代码。

push byte 6
push byte 1
push byte 2
mov ecx, esp
mov ebx, 1
mov eax, 102
int 80h 

这段代码我们可以反这看,最后一行是系统调用,那我们就要看上面eax寄存器中放着什么值,然后看对照表,了解具体信息,这里是102,表示是关于socket的一些操作,如果你看了代码,会发现所有关于socket的系统调用都是102,不是一个完整的socket还需要其他函数吗?这里怎么就唯独厚爱102。其实不然,还需要看ebx,根据他的值来改变内部行为,比如监听、发送、接收、关闭。

这里ebx是1,表示创建socket,那我如何找他其他对应的值呢?

还是需要一个对照表,可以参考下面这个链接。

github.com/torvalds/li…

由于比较少,这里贴出来.

#define SYS_SOCKET	1		/* sys_socket(2)		*/
#define SYS_BIND	2		/* sys_bind(2)			*/
#define SYS_CONNECT	3		/* sys_connect(2)		*/
#define SYS_LISTEN	4		/* sys_listen(2)		*/
#define SYS_ACCEPT	5		/* sys_accept(2)		*/
#define SYS_GETSOCKNAME	6		/* sys_getsockname(2)		*/
#define SYS_GETPEERNAME	7		/* sys_getpeername(2)		*/
#define SYS_SOCKETPAIR	8		/* sys_socketpair(2)		*/
#define SYS_SEND	9		/* sys_send(2)			*/
#define SYS_RECV	10		/* sys_recv(2)			*/
#define SYS_SENDTO	11		/* sys_sendto(2)		*/
#define SYS_RECVFROM	12		/* sys_recvfrom(2)		*/
#define SYS_SHUTDOWN	13		/* sys_shutdown(2)		*/
#define SYS_SETSOCKOPT	14		/* sys_setsockopt(2)		*/
#define SYS_GETSOCKOPT	15		/* sys_getsockopt(2)		*/
#define SYS_SENDMSG	16		/* sys_sendmsg(2)		*/
#define SYS_RECVMSG	17		/* sys_recvmsg(2)		*/
#define SYS_ACCEPT4	18		/* sys_accept4(2)		*/
#define SYS_RECVMMSG	19		/* sys_recvmmsg(2)		*/
#define SYS_SENDMMSG	20		/* sys_sendmmsg(2)		*/

如果你了解socket函数,那么socket函数接收3个参数,下面是他的定义。

 int socket(int family , int type , int protocol );

family表示协议族,目前常用的就是IPv4、IPv6

type表示套接字类型,有流式套接字或者数据报等

protocol表示类型,比如TCP或者UDP。

而这些都有一个与之对应的数字常量。

比如当使用IPv4时候,对应的下面这个。

#define AF_INET		2	

使用流式套接字对应的是下面这个

#define SOCK_STREAM 1

使用TCP的时候,对应的是这个。

#define IPPROTO_TCP = 6,   

所以会看到下面通过push 入栈这些操作,最后吧esp栈顶赋值给ecx,三个参数将传递给socket函数。

push byte 6
push byte 1
push byte 2
mov ecx, esp

最后通过int 80h后 eax寄存器中保存这返回值,被称为socket描述符。

其余socket相关函数也是同样的道理。

最后就是读取请求体,解析出请求资源路径,这里都很简单,思路是找到第一个空格,从这个空格开始依次安字节传送给requestPath变量,直到遇到下一个空格结束。

在定义一个fullPath,他是由root+requestPath得到的完整路径。通过movsb指令将两个值拼接在一起即可,返回响应时,先更具3号中断读取文件,然后再次拼接成一个完整的HTTP响应格式并输出到socket描述符中。

运行

nasm -f elf socket.asm
ld -m elf_i386 socket.o -o socket
./socket 

在浏览器访问http://localhost:9001/3.txt 即可下载路径/home/HouXinLin/test/3.txt的资源

但这里还有很多欠缺,比如文件只能读取40960字节,而且只能请求一次就退出了,在以后的文章中,会通过fork创建一个进程来处理请求,主进程则继续等待客户的请求