前言
本文介绍的是在纯32位汇编下实现MySQL查询,并通过HTTP返回给客户端,本文设计的知识点不多,分别如下:
- nasm 32位汇编语法
- http请求、响应报文
- mysql认证、查询、响应报文
- linux下系统调用
- 基础的socket通信
下面分别一一简单介绍
NASM
汇编语言就不介绍了,nasm也是汇编器的一种,其他的还有masn,它针对windows,还有gas,本文使用的nasm可以在linux上汇编,也可以在windows上汇编,由于在linux上写比较简单,所以本文最终实现是在linux上,只要有一个linux环境+mysql环境就可以运行。
下面介绍一个hello nasm的例子。
nasm汇编基本格式通常由以下几部分组成:
- 数据段:用于声明带有初始化值的数据元素
- bss段:用于声明零值初始化的数据元素
- 文本段:写代码的的地方
section .data
hello_nasm db "hello nasm", 0
section .bss
section .text
global _start
_start:
mov eax,0x4
mov ebx,1
mov ecx,hello_nasm
mov edx,10
int 0x80
通过以下命令即可运行
nasm -g -f elf32 enter.asm && ld -m elf_i386 enter.o
/a.out
hello nasm zsh: segmentation fault (core dumped)
运行后,虽然输出了hello nasm,但是同时伴随着一个错误,这是因为程序没有正确退出导致的,在linux下,程序要正确退出,需要执行sys_exit系统调用,下面会介绍linux的系统调用。
除此之外,还需要了解函数的调用、栈的管理。
函数调用使用 call 关键字,它会将当前指令的下一条地址(也称为返回地址)压入栈中,然后跳转到指定的目标函数地址执行。当目标函数执行完成后,通过 ret 指令将栈顶的返回地址弹出并跳转回去继续执行,所以在目标函数中,不能瞎push,如果有push,在ret前面,一定要pop(出栈),或者进行add esp n,其中n是所有 push操作的字节总数,总之要注意栈平衡,否则ret后跳转的内存错误,程序也就崩溃了。
通常实现的时候会有这样一种规范。
fun_test:
push ebp
mov ebp,esp
sub esp,12
.....
leave
ret
这种规范是典型的 函数调用栈帧管理 模式,称为 标准调用约定。当然不遵循也可以。
它的主要作用是为函数分配独立的栈帧,便于管理局部变量和保存调用环境,随便反编译一些程序,查看其中的汇编,都会看到这样的身影。这种方式会更清晰的管理栈,通过 ebp 和 esp 明确划分栈帧,方便访问参数和局部变量。
下面详细介绍下这几个指令:
-
push ebp
将调用者的栈帧指针(ebp)压入栈中,保存当前函数的调用环境。这样,在函数返回时可以恢复到调用者的栈帧。 -
mov ebp, esp
将当前栈指针(esp)的值赋给栈帧指针(ebp),建立当前函数的栈帧基地址。之后,通过ebp可以访问函数的参数和局部变量。 -
sub esp, 12
向栈中分配 12 字节的空间,用于存储局部变量。此时,esp指向局部变量区域的底部。 -
函数体部分
具体函数实现的操作,例如对局部变量的操作可以通过[ebp-4]、[ebp-8]等访问,函数参数可以通过[ebp+8]、[ebp+12]等访问(调用约定决定参数的位置)。 -
leave
恢复调用者的栈帧,等价于:mov esp, ebp:将栈指针恢复到当前函数栈帧基地址。pop ebp:弹出调用者的栈帧指针到ebp中。
-
ret
从栈中弹出返回地址并跳转到该地址,恢复到调用者代码的执行位置。
HTTP报文
http报文部分是本文最简单的一部分,简单说就是http请求发送给后端的完整数据,和后端响应给http客户端的完整数据,通常在框架的加持下,我们只关心请求体、请求参数、响应体部分,其他的数据由框架为我们拼接。
如下,这是http请求报文的完整格式
这个是http响应报文格式
在本系统中,我们不需要解析所有请求数据,只要有请求进来,就返回给查询到的数据即可。
MySQL协议
这是本文的第二难点,需要参考MySQL协议的格式,解析出数据,其中最难的是加密部分,由于MySQL 8 默认的认证方式是 caching_sha2_password,这个方式涉及到了SHA-256 的加密算法,也就是说,要使用汇编实现一个加密算法,当然可以改成mysql_native_password,这个加密比较简单,但是既然都用汇编写了,那就难度直接拉满,使用caching_sha2_password。
MySQL通信分为5部分,下面使用Wireshark抓包的结果。
- 客户端连接到MySQL服务器,MySQL会先返回一些服务器信息、加密方式、还有随机数种子(用于加密)。
3. 客户端通过SHA-256加密密码,还有用户名信息,发送给MySQL服务器,当然这里还不单单是对密码加密,还要和上一步返回的随机数进行xor。最终的算法是这样的。
XOR(SHA256(password), SHA256(SHA256(SHA256(password)) + 随机数))
5. 如果MySQL服务器校验通过,会返回一个Ok包,并等待客户端发送SQL信息
- 客户端构建SQL命令,发送给服务器
9. 服务器返回查询到的字段、行信息给客户端,客户端按照约定的格式解析即可
当然在实际中,还要有许多细节要处理, 比如单元格数据超过255,又是另外一种情况,另外要对响应的错误码做不同逻辑,这些小细节在本文会全部忽略。
linux系统调用
本系统中有大量的系统调用,系统调用就是调用linux提供的函数,比如write、read等,在本系统涉及到了socket的调用、read、write等的调用,其实本例子中大部分操作都是在读和写。
系统调用触发是使用int 0x80,具体调用的功能号放在eax中,比如4就是输出,另外的参数放在ebx、ecx、edx、esi、 edi,但是可以发现,最大的数量就是6个参数,如果超过6个,可以使用ebx寄存器保存指向输入参数的内存位置,按照参数的连续的顺序存储即可。
关于系统调用号,可以在这里查看
如果要查看参数,可以直接在linux库中搜索,比如sys_write的参数。
socket通信
在汇编下实现socket的创建、读写比较简单,难得部分是解析数据,拼接数据,汇编没有像高级语言那种"h"+"e"+"l"+"l"+"o"就可以拼接出"hello",如果在汇编中写的话,可能需要几十行。
socket server创建流程大家应该也熟悉,就是create socket、bind、listener、accept,然后对客户端的描述符进行read、write。
代码实现
大致的流程是,创建服务器socket,然后等待客户端的连接,如果有请求,则连接到mysql,执行select查询后,将拼接的数据输出到客户端描述符号中。
效果如下,将从数据库里面通过sql select * from user.users;查询所有的用户名并且返回
代码过长不宜展示,移步到github: github.com/houxinlin/n…
程序默认启动的端口是8080,并且连接127.0.0.1:3306的mysql
运行方式:
nasm -f elf32 server.asm
ld -m elf_i386 server.o
./a.out
curl http://localhost:8080