基于x86 实现编译器首先得了解x86 平台的指令调度, 以下是一些我在实现编译器过程中用到的基础知识!
大部分内容参考了 青木的自制编译器,
c语言使用gcc 编译, 而gcc 编译器将c代码转化为汇编之后, 也是用的GNU as 的汇编器将汇编语言 .s 转化为.o,然后再连接, 因此如果自制编译器, 也是做到汇编这一步, 所以又得学x86 汇编器的使用方法!
gcc 用的 GNU, 那我就直接使用gcc!
编译:
as hello.s //默认生成 a.out
也可以指定名字:
as -o hello.o hello.s
生成目标文件后还要连接才可以执行。
gcc hello.o -o hello
最后就是
./hello
指令包括 助记符 和 操作数 组成,比如 movl %esp, %ebp
, movl 助记符, 后面两个为操作数, 操作数可以为多个!逗号分隔。
这个是由cpu直接执行的指令
以点 “.” 开头的, 末尾没有冒号 “:” 的 行 都是汇编伪操作行。
由汇编器执行,记录元数据和设定指令的属性。
汇编的首行缩进是没有影响的
以冒号“:” 结尾的 行 都是标签行
冒号是语法, 标签名为冒号前面的值。
一般命名加上 . ,避免和代码里面的名字重复。
后缀 | 大小 |
---|---|
b | 8 |
w | 16 |
l | 32 |
使用$num 来标识, $1 表示 1
可以直接当做立即数, 使用 % 作为前缀, %eax = %EAX
直接访问固定内存
相当于使用c语言的数组
例子:
movl %ecx, %eax
最后,将ecx 寄存器中的数据作为地址访问内存,并将内存上的数据加载到eax 寄存器中
的写法如下所示。
movl (%ecx), %eax
不习惯汇编的话会觉得%ecx 和(%ecx) 的区别难以理解,可以把它当作C 语言的指针。
指针变量ptr 自身的值等同于%ecx 的话,那么对指针的取值操作*ptr 就相当于(%ecx)。
另外,%ecx 是访问寄存器,而(%ecx) 则是利用寄存器访问内存。
//todo important
mov 是在寄存器或内存之间传输数据,或者将立即数加载到寄存器或
内存的指令。mov 也是汇编语言中最常用的指令之一。
mov 立即数,寄存器
mov 寄存器, 寄存器
mov 内存,寄存器
mov 立即数,内存
mov 寄存器,内存
mov 内存,内存
push 立即数
push 寄存器
push 指令将数据压栈。具体来说,将esp 寄存器减去压栈的数据的大小(注意会自己减去),再将数据存储到
esp 寄存器所指向的地址。
pop 寄存器
pop 指令将数据出栈并写入寄存器。具体来说,将数据从esp 寄存器所指向的地址加载到
寄存器,再将esp 寄存器加上出栈的数据的大小。
lea 内存,寄存器
lea 指令将地址加载到寄存器。lea 是Load Effective Address(实效地址加载)的简称。
“lea 内存, 寄存器”将内存对应的地址加载到寄存器。
note:
mov 指令表示将ebx 寄存器加4 后的值作为内存地址进行访问,并将数据加载到eax 寄存器中。
movl 4(%ebx), %eax
另一方面,将上述语句中的mov 指令替换为lea 指令,如下所示。该语句表示将ebx 寄存
器加上4 后的值保存到eax。
leal 4(%ebx), %eax
同样是间接内存引用的语句,mov 指令取得的是内存地址所指向的内存上的数据,而lea
指令取得的是内存地址本身。
//! important
add 立即数, 寄存器
add 寄存器, 寄存器
add 内存, 寄存器
add 立即数, 内存
add 寄存器, 内存
add 指令将第1 操作数和第2 操作数相加,并将结果写入第2 操作数。
请注意“将运算结果写入第2 操作数”这一点。
例如add$1, %eax 表示将eax 寄存器的数据加1,并将结果保存到eax 寄存器。类似于C 语言中的+= 运算。
addl $1, %eax # 将eax 寄存器加1
addl %ecx, %eax # eax 寄存器和ecx 寄存器的数据相加后存放到eax 寄存器
addl $4, (%ebx) # 将ebx 寄存器所指向的内存中的数据加4
加减乘除差不多!!
指令 | 功能 |
---|---|
jmp | 无条件跳转 |
jz、jnz、je、jne | 条件跳转 |
cmp | 数据的比较 |
test | 数据的非0 检查 |
sete、setne、setg、setge、setl、setle | 获取eflags 寄存器中的各个标志位 |
call | 函数调用 |
ret | 从子程序返回 |
其它指令类似!
代码段 | 功能 | 对应汇编格式 |
---|---|---|
bss段 | BSS段通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文BlockStartedby Symbol的简称。BSS段属于静态内存分配。 | .bss |
data段 | 数据段通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。 | .comm |
text段 | 代码段通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。 | .text |
rodata段 | 存放C中的字符串和#define定义的常量 | .rodata |
stack栈 | 用户存放程序临时创建的局部变量 | push和pop使用 |
经过我无数次直接从gcc 生成汇编观察出, 调用函数, 首先是参数压栈处理然后, ebp 的值压栈, 接下来返回地址压栈, 用一个图表示:
当调用函数时, 先扩大栈空间, 把参数压栈, 然后在被调用函数里面使用参数记得是 ebp 加上 8, 中间有返回地址和旧ebp的值!
x86所有的寄存器是通用的, 想用那个就用哪个!
截取自ecc
# asign
movl 8(%esp),%esi
leal 12(%esp),%edi
movl %esi,(%edi)
其它类似。。。我用gcc 生成汇编,一步一步摸索出来的。。。