前言
先看一下整体代码,以下是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,具体对照表可以参考下面的网址
这样在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,那我如何找他其他对应的值呢?
还是需要一个对照表,可以参考下面这个链接。
由于比较少,这里贴出来.
#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创建一个进程来处理请求,主进程则继续等待客户的请求