二进制基础
从C语言到可执行程序
- 源代码C Code —(编译)— 汇编代码 —(汇编)— 目标文件 —(链接)— 可执行程序
汇编语言是机器指令的助记语言,汇编语句和机器语句可一一对应
目标文件是源代码经过编译后没有被链接的那些中间文件,例如Linux下的.o文件
可执行程序是可被操作系统加载到内存并执行的文件,例如Linux系统下的ELF文件和Windows下的EXE文件
库函数是可复用的代码(包含函数或变量)集合,用于向其他程序提供代码
库分为静态库(.a)和动态库(.so):静态库中的代码和数据会完整的拷贝到可执行程序中,而动态库则需要在程序运行时通过动态链接的方式加载
动态库的目标是为了节约操作系统运行时的内存和文件的内存,如大量程序内都会用到的一些常规函数,不比出现多份,动态链接的机制可以帮助你复用这些代码
无论是何种方式,链接的作用都是修复文件之间的引用关系
机器指令的执行
- 取指令 → 指令译码 → 指令执行 → 访存取数 → 结果写回
CUP从内存中读取指令并对指令进行解码,去执行,并在最后写回。执行过程中会涉及内存中其他数据的读取,执行结果有可能涉及一些结果的写回
寄存器:CPU片上的一些存储单元(存储能力有限,但访问速度很快,一些不需要返回到内存中的中间值可以储存到寄存器中来减少CPU的访存次数提高运行速度)
栈
一种先进后出的数据结构
被用于函数的局部内存管理
- 保存局部变量
- 保存函数的调用信息(如返回地址)
栈往低地址方向增长,栈底高地址,栈顶低地址
esp寄存器永远指向栈顶
栈操作
入栈,Push:esp = esp - 4(32位下是4字节) 出栈,Pop: esp = esp + 4 无论是存入数据还是取出数据,这些行为都是在栈顶进行的
-
栈是从高地址向低地址增长,在每一层栈中会保存参数,返回地址,栈帧指针(ebp),被调用者保存的寄存器,局部变量这些数据。这里引入栈帧的概念
-
栈帧(Stack Frame):在函数调用过程中产生的局部内存信息。当函数的调用深度越深,栈帧依次排布,一层一层逐渐向下排布。调试时可以根据一层一层栈帧的顺序来查看信息
常见指令
- 数据迁移指令
mov:把数据从一个地方移到另一个地方。用于立即数,寄存器,内存三个地方拷贝数据
立即数寻址:操作数包含在指令中,紧跟在操作码后,作为指令的一部分
mov al,5//把5放到al里面
mov eax,1000h
寄存器寻址:操作数在寄存器中,指令指定寄存器
mov ax,bx//把bx里的数据放到ax中
mov ebp,esp
直接内存寻址:操作数在内存中,指令直接指定内存地址
mov ax,[2000h]//从2000h这个地址的操作数拿到ax中
寄存器间接寻址:操作数在内存中,操作数的地址在寄存器中
mov eax,[ebx]//把ebx指向的内存的数据存到eax中
索引寻址:通过基址寄存器内容加上一个索引值来寻址啊内存中的数据
mov ax,[di+100h]
相对基址索引寻址:用基址寄存器+变址内存器的内容+偏移量 完成内容单元的寻址
mov dh,[bx+si+10h]
比例寻址变址:基址寄存器+变址寄存器的内容和比例因子的乘积 来完成内容单元的寻址
mov eax,[ebx+4*ecx]
push:入栈操作,向栈顶存入一个数。
push<reg32>==sub esp,4;mov [esp],<reg32>
push<mem>
push<con32>
pop:push的逆运算,从栈顶取出一个数存到其他地方
pop<reg32>
pop<mem>
lea:加载有效地址。
lea目标,源
lea<reg32>,<mem>
lea eax,[var]//将地址var放入寄存器eax中
lea edi,[ebx+4*esi]//等价于edi=ebx+4*esi
#一些编译器会使用lea指令进行算数运算,因为速度更快
- 算数与逻辑指令(用法与数据转移类似)
add/sub
inc/dec
imul/idiv
and/or/xor
not/neg
shl/shr
- 控制转移指令
jmp:无条件跳转
j[condition]:条件跳转(if/else语句会生成这样的指令)
cmp:比较两个操作数
call/ret:函数调用/函数返回(和jmp区别是保存了返回的地址)
汇编指令的两种语法
Intel | AT&T |
---|---|
mov eax, 8 | movl $8, %eax |
mov ebx, 0ffffh | movl $0xfffff, %ebx |
int 80h | int $80 |
mov eax, [ecx] | movl (%ecx), %eax |
- 操作数顺序不同
- 寄存器记法不同
- 立即数记法不同
- 访存寻址计法不同
- 操作码助记符不同
由于汇编指令与机器语言联系紧密,具体可在使用时检查手册
不同的CPU有不同的指令集,如果都是x86的话,指令集应该是一样
不同的汇编器有不同的伪指令,编写的汇编语言格式有可能不同
不同的操作系统,对操作系统调用的方法和过程有可能不同
调用约定
-
什么是调用约定?
函数调用约定,是指当一个函数被调用时,函数的参数会被传递给被调用的函数和返回值会被返回给调用函数。函数的调用约定就是描述参数是怎么传递和由谁平衡堆栈的,当然还有返回值
- 实现层面(底层)的规范
- 约定了函数之间如何传递参数
- 约定了函数如何传递返回值
-
常见x86调用约定
- 调用者负责清理栈上的参数(Caller Clean-up)
- cdecl
- optlink
- 被调用者负责清理栈上的参数(Callee Clean-up)
- stdcall
- fastcall
- 调用者负责清理栈上的参数(Caller Clean-up)
进程内存空间布局
-
常见x86调用约定
- 调用者负责清理栈上的参数(Caller Clean-up)
- cdecl
- optlink
- 被调用者负责清理栈上的参数(Callee Clean-up)
- stdcall
- fastcall
- 调用者负责清理栈上的参数(Caller Clean-up)
进程内存空间布局
对于不同进程来说,进程内存空间布局是大致相同的。同一地址其实是虚拟内存,数据被存放到不同的物理内存当中并不会冲突,而虚拟内存与物理内存之间的对应工作由操作系统来完成(例:大部分程序代码从0x08048000开始存放代码)