以下内容参考自data whale 组队学习
编译系统与指令
C语言的编译
~ gcc -Og -o prog main.c mstore.c
* -Og:用来告诉编译器生成符合原始 C 代码整体结构的机器代码(实际可能使用-O1/-O2等)
* -o prog 表示生成可执行文件的文件名
~ vim mstore.c

• pushq 这条指令的意思是将寄存器 rbx 的值压入程序栈进行保存(这里的q表示64位数据类型,四字)
寄存器的名称:
Intel x86-64 的处理器中包含了 16 个通用目的的寄存器,这些寄存器用来存放整 数数据和指针。为:%rax, %rbx, %rcx, %rdx %rsi, %rdi, %rbp, %rsp, %r8, %r,9 %r10, %r11, %r12, %r13, %r14, %r15
其中:
通用寄存器:rax, rbx, rcx, rdx
栈寄存器:rsp(栈顶指针寄存器), rbp(栈基址寄存器)
源变址和目标变址的寄存器: rsi, rdi
指令寄存器:rip
传参寄存器:rcx,rdx, r8, r9
scratch寄存器:rbx,r12, r13, r14, r15(scratch),需要保护
两种保存寄存器(为了调用函数中值不改变):
* 函数 B 在使用寄存器 rbx 之前,先保存寄存器 rbx 的值,在函数 B 返回之前, 先恢复寄存器 rbx 原来存储的内容,这种策略被称之为被调用者保存。(%rbx,%rbp, %r12, %r13, %r14, %r15)
* 函数 A 在调用函数 B 之前,提前保存寄存器 rbx 的内容,执行完函数 B 之后, 再恢复寄存器 rbx 原来存储的内容,这种策略就称之为调用者保存;(其他)
• movq:将寄存器 rdx 的内容复制到寄存器 rbx。
• call 指令对应于 C 代码中的函数调用,该函数的返回值会保存到寄存器 rax 中(x 和 y 的乘积结果)。
• 第二个movq:将寄存器 rax 的值送到内存中,内存的地址就存放在寄存器 rbx 中。
• pop 指令,恢复寄存器 rbx 的内容
• ret 就是函数返回。
二元操作指令:指令 源操作数(立即数、寄存器、内存) 目的操作数(寄存器、内存)
一元操作指令:该操作数既是源操作数也是目的操作数,操作 数可以是寄存器,也可以是内存地址。
ALU 除了执行算术和逻辑运算指令外,还会根据该运算的结果去设置条件码寄存器、以及跳转指令。
条件码:条件码寄存器它是由 CPU 来维护的,长度是单个比特位,它描述了最近执行操作的属性;
跳转指令:跳转指令会根据条件寄存器的某种组合来决定是否进行跳转。(这里注意:基于条件传送的代码会比基于跳转指令的代码效率高:当遇到条件跳转时,处理器会根据分支预测器来猜测每条跳转指令是否执行,当发生错误预测时,会浪费大量的时间,导致程序性能严重下降。 )
循环语句是通过条件测试+跳转的结合;switch:跳转表实现(复杂分支高效)
C语言的反汇编
机 器代码翻译成汇编代码。
~ gcc -Og -c mstore.c (二进制机器码文件)
~ objdump -d mstore.o (反汇编)
过程
过程提供了一种封装代码的方式,它可以隐藏某个行为的具体实现,同时提供清晰简洁的接口定义。如 C 语言中的函数,Java 语言中的方法等。
多个过程调用,存在传递控制,传递数据,分配和释放内存机制。
内存管理机制:运行时栈
栈帧:当函数执行所需要的存储空间超出寄存器能够存放 的大小时,就会借助栈上的存储空间,我们把这部分存储空间称为函数的栈帧。
转移控制实现:
* 指令 call 不仅要将函数的第一条指令的地址写入到程序指令寄存器 rip 中,以此实现函数调用。同时还要将返回地址压入栈中。
* 当函数执行完毕,指令 ret 从栈中将返回地址弹出,写入到程序指令寄存器rip中。
参数传递实现:
* 参数 1 参数 6 的传递可以使用对应的寄存器(注意:寄存器的使用是有特殊顺序规定的,此外,寄存器名字的使用取决于传递参数的大小)。
* 如果一个函数的参数数量大于 6,超出的部分就要通过栈来传递。(注意:通过栈来传递参数时,所有数据的大小都是向 8 的倍数对齐)。
局部变量使用地址运算符时,需要在栈上为这个局部变量开辟相应的存储空间。
局部变量是不需要对齐(8个字节的倍数)。
递归调用:每次函数调用都有它自己私有的状态信息,栈分配与释放的规则与函数调用返回的顺序也是匹配的,所以次数过大时不建议递归。
指针运算:
对指针进行运 算时,计算结果会根据该指针引用的数据类型进行相应的伸缩。
case:数组(定长、变长)(多维),结构体
结构体的数据对齐:int型的起始地址必须是4的倍数,所以编译器会再该变量插入间隙达成这个效果
实际上(struct):对于不同的数据类型,地址对齐的原则是任何 K 字节的基本对象的地址必须是 K 的倍数。也就是说对于 short 类型,起始地址必须是 2 的倍数;对于占 8 个字节的 数据类型,起始地址必须是 8 的倍数。(结构体末尾也需要)(结构体可以通过调整排列顺序,使得所有的字段都满足了数据对齐的要求。)
联合体(union):联合体中的所有字段共享同一存储区域,因此联合体的大小取决于它最大字段的大小。
联合体的应用情况:1)事先知道两个不同字段的使用是互斥的,可以将这两个字段声明为一个联合体。原理就是不会让不可能有数据的字段白白浪费内存。2)可以用来访问不同数据类型的位模式
缓冲区溢出:数据长度超过声明长度。超过以后,返回地址以及更多的状态信息会被破坏,那么返回指令会导致程序跳转到一个完全意想不到的地方。
这是病毒的一个常用手段,对此限制方法有:
* 栈随机化:栈的位置在程序每次运行时都有变化
* 栈破坏检测:在缓冲区与栈保存的状态值之间随机产生并存储一个特殊值(金丝雀值),在函数返回之前,检测金丝雀值是否被修改来判断是否遭受攻击。
* 限制可执行代码区域:处理器的内存保护引入不可执行位,将读和可执行访问模式分开。由此栈可以被标记为可读和可写,但是不可执行。检查页是否可执行由硬件来完成,对性能基本无影响。