声明
毕生所学!术语不对,做法不恰当也没办法,毕竟只知道这么点。
基础
参考学长发的汇编入门
函数
可能上面的教程差的就是函数怎么写了
首先两个寄存器很重要ebp
和esp
ebp
表示当前函数的栈空间的首地址
esp
是当前栈顶
push
的时候就会把esp
减4,注意是减,不是加,栈从高地址向低地址扩展
也可以sub esp, 0x4
,分配局部变量的空间的时候就是这样做的
// 一个递归的函数
fun(int v1, int v2)
{
v1 = v1 + 1;
if (v1 <= v2)
v1 = fun(v1, v2);
return v1;
}
简化版的汇编代码,v1,v2都没有存到栈里
global main
main:
push ebp
mov ebp, esp
push 7; 传入参数v2
push 3; 传入参数v1
; 传参数是从右向左传
call fun
mov esp, ebp
pop ebp
ret
fun:
; 保存现场的操作
push ebp ; 把上一个函数的基址寄存器存起来
mov ebp, esp ; 把当前ebp 设为当前栈顶esp
; 这里是函数内部的代码
; 关于局部变量,仅作示例,在fun中无含义
sub esp, 0x8 ; 给局部变量分配0x8字节的空间, 比如int a, b;
mov eax, [ebp-0x4] ; 访问第一个4字节的局部变量,即访问 a
mov eax, [ebp-0x8] ; 访问第二个4字节的局部变量,即访问 b
; 关于访问传入的参数
mov eax, [ebp+0x8] ; 访问最后一个push的参数,即v1
mov ebx, [ebp+0xc] ; 访问倒数第二个push的参数,即v2
add eax, 1
cmp eax, ebx
jg out
push ebx
push eax
call fun
out:
; 返回值约定保存在eax上,所以不用动了
; 恢复现场的操作
mov esp, ebp ; 意思就是不管你esp怎么减,或者不管push,pop了多少次,都把栈顶恢复到原来的情况
pop ebp
ret ; 返回
编译执行
nasm -f elf test.asm -o test.o ; gcc -m32 test.o -o test
./test ; echo $? # 执行并打印eax
./test # 执行
注意事项
操作数限制
一些指令的某些操作数必须要是寄存器或者常量
一些指令的某些操作数必须要是寄存器
一些指令必须至少要有一个寄存器的操作数
具体的我也不清楚,我都是试出来的
如
mov [a], [b] ; 就不行
; 就必须
mov eax, [b]
mov [a], eax
db, dw, dd
word 是2个字节
dword 是4个字节
然后dw是指word而不是dword,dd才是指dword
所以要声明全局int变量,要用dd
参考
; 声明4字节全局变量a
section .data
a dd 0
idiv
有符号数除法
具体的自己搜一下这个指令吧
idiv 如果使用一个操作数的用法
如idiv ebx
表示 edx: eax / ebx,结果商存到eax,余数存到edx上
edx 是高32位,eax是低32位
需要一个cdq指令把edx设置为恰当的指
如果eax为正数,则edx设为0;如果eax为负数,则edx设为0xffffffff
示例
int div_mod(int a, int b)
{
print_int(a / b);
print_int(a % b);
}
void main(){
div_mod(7, 3);
}
extern printf
global main
div_mod:
push ebp
mov ebp, esp
mov eax, [ebp+0x8]
mov ebx, [ebp+0xc]
cdq ; 给edx 设置恰当的值,所以需要的话要先把edx的值存起来
idiv ebx
push eax ; 传入商
call print_int ; 打印
mov eax, [ebp+0x8]
mov ebx, [ebp+0xc]
cdq
idiv ebx
push edx ; 传入余数
call print_int ; 打印
mov esp, ebp
pop ebp
ret
main:
push ebp
mov ebp, esp
push 3
push 7
call div_mod
mov esp, ebp
pop ebp
ret
print_int:
push ebp
mov ebp, esp
mov eax, [ebp + 0x8]
push 0x0a6425
mov ebx, esp
push eax
push ebx
call printf
mov esp, ebp
pop ebp
ret
调用外部函数
使用printf实现的print_int
extern printf
global main:
print_int:
push ebp
mov ebp, esp
mov eax, [ebp + 0x8]
push 0x0a6425 ; 这个其实就是“%d\n”,不想用全局变量了
mov ebx, esp
push eax
push ebx
call printf
mov esp, ebp
pop ebp
ret
main:
push ebp
mov ebp, esp
push 3
call print_int
mov esp, ebp
pop ebp
ret
这个函数用printf可以打印四个寄存器的值
extern printf
global main:
print:
push ebp
mov ebp, esp
push edx
push ecx
push ebx
push eax
push format
call printf
pop eax
pop eax
pop ebx
pop ecx
pop edx
mov esp, ebp
pop ebp
ret
main:
push ebp
mov ebp, esp
mov eax, 1
mov ebx, 2
mov ecx, 3
mov edx, 4
call print
call print
mov esp, ebp
pop ebp
ret
section .data
format db 'eax:%d ebx:%d ecx:%d edx:%d', 0xa, 0 ; 0xa是‘\n’, 0是‘\0’
总结
写一下总结吧
看起来汇编的东西也就只有这么点。
前面的抽象语法树的设计还比较完善,后面用起来还是比较好用的。
没有生成中间代码,就直接输出汇编代码了。
没有中间代码,就没法优化,不过直接写汇编这么底层的东西,各种功能想要实现还是都可以实现的。虽然还有一些bug,但是不管了。
主要的精力还是花在设计上了,设计就差不多得凭空弄出一个东西来。最后还是自顶向下大法好,自顶向下过一遍,就发现了需要哪些功能,凭空就弄出一个操作数语义的类来,接口一设计一实现,后面就是利用这些接口的事情了。再根据出现的bug修修补补,就差不多了。