精通细节是理解更深和更基本概念的先决条件, 魔鬼隐藏在细节中
机器代码的产生过程
机器代码: 用字节序列编码低级的操作,包括处理数据、管理存储器、读写存储设备上的数据、以及利用网络通信。
编译器机基于编程语言的原则、目标机器的指令和操作系统遵循的原则, 经过一系列的阶段产生机器代码。GCC
C语言编译器以汇编代码的形式产生输出,然后调用 汇编器和链接器 从而根据汇编代码生成可执行的机器代码。
抽象
指令集
操作数 源数据: 常数、寄存器、存储器
操作数 类型: 立即数(常数)、寄存器、存储器
数据传送指令: mov
算数逻辑操作: add, sub, imul, sal, shl, leal, imull, mull, idivl, divl
控制:条件码,跳转指令,test, sete, sets, setg etc, cmp, jmp, 条件码一般使用比较、算数、直接设定三种方式, 跳转指令则利用条件码来进行跳转或者间接跳转
栈: push, pop
C 语言的指针就是地址,间接引用指针就是将该指针放在一个寄存器中,然后在存储器引用中,使用这个寄存器, 局部变量通常保存在寄存器中。
C 语言 控制结构 汇编表示
switch 的实现
使用跳转表实现,来达到执行时间跟开关数量无关。
条件传送指令
因为现代处理器的流水线设计,导致在条件判断时候,才能确定下一条执行指令的位置,而导致按照顺序执行
准备的代码可能被抛弃,而对应的准备工作则变为了浪费。 而 条件传送 指令先计算出条件操作的两种结果,然后根据条件来选择满足的结果。从而避免了
因为跳转指令 带来的资源浪费。另一方面现代处理器都采用了 分支预测 逻辑,来试图猜测每条跳转指令是否被执行。(处理器设计试图达到
90%的正确率),正确的预测可以没有代价,然而额错误的预测则会带来严重恩惩罚,大约 20-40 的时钟周期的浪费,导致性能严重下降。
举例: 例如简单 三目运算符, x > y ? x+y : x-y, 当两个表达式具有副作用的时候则不能应用。
结构实现
数组分配和访问: 基本实现为, 在存储器中分配一个连续的 T A[N], L * N 字节的连续大小的空间。 L为T类型的字节大小。而C语言中数字指针的实现(ptr ++ )则实现为单纯的 地址运算。嵌套数组 则以 行优先、列优先 的方式进行展开。Struct 的实现, 变量为 首地址 + 偏移量。
过程实现
过程:
1. 函数调用过程的两个寄存器 %ebp(帧指针), %esp(栈指针)
2. 帧指针保存当前过程的最高位置,%esp则向下增长,
用于分配必要的地址空间,调用函数参数等。
3. 调用过程:
调用前, 首先压入调用参数,返回地址, 压入%ebp,
调用后,将 %ebp 重置为当前的%esp,
标记确定当前的 函数的最高地址。
返回时, movl %ebp, %esp; popl %ebp; ret;
恢复调用函数之前的样子。
天生的具有递归属性。
1. 什么时候需要帧指针:
2. X86-64 中对于过程的 一些具体优化:
参数通过寄存器传递到过程,而不是在栈上,消除了在栈上存储和检索值的开销
call 指令将一个64位的返回地址存储在栈上
许多函数不需要栈帧,只有那些不能将所有局部变量存储在寄存器中的函数才需要在栈上分配空间
没有帧指针,作为替代,对栈位置的引用相对于栈指针。
C 语言 指针
每个指针都对应一个具体的类型: 指针类型不是机器代码中的一部分,C语言提供的一种抽象,地址运算,来避免寻址错误。
每个指针都有一个值, 这个值是某个指定类型对象的地址。
指针用& 运算符创建
运算符 * 用于指针的 间接引用
数组与指针紧密关联
指针类型转换: 只改变类型,而不是值
指针可以指向函数,指向函数机器代码中的第一条指令地址
C语言跟汇编指令 的差别很大,在汇编语言中,各种数据类型之间的差距很小,程序以指令序列来表示。每条指令是一个单独的操作。编译器必须提供多条指令来产生和操作各种数据结构,来实现像条件、循环、和过程这样的控制结构、抽象机制。