gcc -Og -o p p1.c p2.c
采用gcc编译c语言java
gcc命令会调用一些列程序:算法
其中目标代码和可执行文件都是机器代码编程
计算机系统对复杂的机器级编程进行抽象:数组
汇编代码接近机器代码,但提供了更好的可读性安全
x86-64机器代码与c差距巨大,处理器中不少状态在c语言隐藏可是在x86-64中均可见:数据结构
程序内存包含了下面几个部分:架构
机器代码须要知道程序的虚拟内存地址便可,由操做系统将虚拟内存地址转为物理内存地址函数
在机器代码中,内存中不存在数据结构和对象,只有一个大字节数组和其中的虚拟地址性能
源代码mstore.c学习
gcc -Og -S mstore.c
:生成汇编文件mstore.s
gcc -Og -c mstore.c
:生成二进制目标代码文件mstore.o
objdump -d mstore.o
:经过反汇编器将机器代码翻译成汇编代码
因为最先从16位体系结构发展起来,Intel称字(word)表示16位数据类型,32位和64位就成为双字和四字
c语言的数据类型与Intel数据类型的映射关系:
汇编代码中操做指令后跟上这些数据类型的后缀,能够代表是操做哪一种类型的数据。如movb表示传送字节
16个整数寄存器分别能够存储64位值,具体用来存整数和指针
大多指令有N个操做数,表明一个操做指令所使用的源数据值和放置的目的位置。操做数有三种类型:
将数据从源位置复制到目的位置的指令,有三种mov,movz,movs:前者作普通拷贝,高位保持不变;后二者用于将较小的源值拷贝到较大的目的地址,并采用0或者符号位扩展高位
mov
源和目的地能够是当即数、寄存器或内存地址,但不能同时是内存地址,拷贝以后不作任何变更(例外:movl的目的地是寄存器时,高四位字节置零)
movz
movs
程序栈存放在内存的某个区域,将栈视为一个大树组的话,栈顶在数组低位,并向“下”扩展
分为三种类型:
leaq:将内存数据读取到寄存器,相似movq
示例:
通常使用leaq运算的指令会比movq的指令更少,所以每每leaq更高效
有两个操做数:
描述64位相乘(得128位数字)和128位整除的指令,思想是将128位数采用两个64位寄存器存储
上述全部指令都是顺序执行,为了支持c语言条件语句、循环语句、分支语句,引入jump指令
### 条件码
除了整数寄存器,cpu还维护一组单个位条件码寄存器,记录最近算术和逻辑运算的状态。检测这些属性来执行条件分支指令,经常使用的四个:
上述指令除了leaq,都会设置条件码
上述指令,CMP相似SUB,TEST相似AND,区别在CMP和TEST只设置条件码而不修改寄存器的值
一般不会直接读取条件码,经常使用的使用方法为:
间接跳转中,区分跳转须要区分跳转目标是从寄存器仍是内存地址读出来的
jmp *%rax
:用寄存器%rax的值做为跳转目的地
jmp *(%rax)
:用寄存器%rax中的值做为地址,读出该地址内存的值,做为跳转目的地
汇编中jmp到目标位置,在机器码中,实现方式为:
示例采用PC相对的编码方式:
PC相对编码更经常使用,相比绝对地址编码的优点在于:
设置条件码,并经过JUMP类型的指令跳转到指定分支。这种方式简单、通用
计算一个条件操做的两种结果,而后根据条件是否知足从中选取一个。这种使用受限,可是性能更优,GCC只要在两个表达式都只是很简单的指令时,才会选择条件传送,不然,绝大多数状况都是采用条件控制
上图列举了x86-64的一些条件传送指令
条件传送高效的秘密:处理器的流水线机制。流水线机制就是将一条指令拆成多个步骤(从内存取指令、肯定指令类型、从内存读数据、执行算术运算、向内存写数据、更新PC),经过重叠连续指令的步骤(执行指令运算的同时取下一条指令),保证流水线充满了待执行的指令,提升了高性能。这样就必须预测后续指令,若是出现条件分支,可能致使预测错误,致使必须丢弃取到的指令并从新获取,致使加大指令执行的时钟周期,致使性能降低
do-while循环:
while循环:
gcc -Og
模式下翻译方式:
gcc -O1
模式下翻译方式:
for循环:
翻译成while循环的两种模式(取决于优化等级)
switch会利用跳转表来实现高效跳转到正确的分支。跳转表就是一个数组,存储了全部分支代码段的地址
上图汇编代码,就是一个跳转表的声明:
上图表是switch语句从原始C代码到汇编代码的翻译过程:
在分支量较大,并且跳转表索引密集的时候,GCC会自动选择跳转表来优化switch语句。经过跳转表直接定位代码段,致使分支较多的状况下,switch语句相比if-else更高效
过程调用的实现机制涉及三个方面(假设过程P调用过程Q,Q执行后返回P):
涉及过程调用、控制转移的指令:
带星号表示间接调用,就是获取给操做数的值做为地址,进行调用
假设存在程序,main函数调用multstore函数:
main
multstore
运行时栈
流程说明(%rip为程序计数器PC的值,%rsp为栈指针的值):
P调用Q,P须要将前6个参数复制到寄存器,从Q返回时,Q须要将惟一的返回值写入%rax
图表中列出参数在指定的寄存器中的存放顺序
当参数N>6时,P须要为7 ~ N个参数分配栈空间,并按N到7的前后顺序压入栈,保证第7个参数在栈顶,只有分配了栈空间,才能将控制转移给Q,Q来访问栈空间上的参数
如图展现,前六个参数在寄存器,后两个在栈
这里最后一个参数不在栈顶而是在%rsp+8的位置,是由于在参数入栈以后,返回地址也入栈了
虽然不少时候局部变量能够存在寄存器,可是如下这些状况须要必须为局部变量分配栈空间:
实例解析1
实例解析2
寄存器是全部过程的共享空间,须要保证过程P调用过程Q的时候,Q不会篡改P等会要用的寄存器
为防止上述状况,x86-64定义了一组规范:
其中着重关注:
保存这些寄存器值的方案:
实例讲解:
函数P中有两个值在汇编代码中存在被修改的风险:
上述两个保存操做使用了%rbp和%rbx这两个被调用者保存的寄存器,为保证P返回后还能正常使用,P须要提早保存它的值
保证递归正常工做的机制:
实例讲解:
每次调用都会将n减1,因此须要提早保留n的值保证后续正常使用
最右边一、八、四、8这些因子,表明char、char指针、int、double指针的字节大小
设E为int类型数组,想要获取第i的元素E[i]。E的地址存放在%rdx,i的值存放在%rcx,则获取E[i]的方式为:
movl (%rdx,%rcx,4),%eax
int A[5][3]
等价于
typedef int row3_t[3]; row3_t A[5];
即:A是个包含5个元素的数组,每一个元素都是一个存储了3个元素的数组
这个嵌套数组A也可视为5行3列的二维数组
A的占用字节为5*3*4=60
公式:对任意二维数组T D[R][C]
,元素D[i][j]的内存地址为&D[i][j]=Xd + L*(C*i + j)
编译器访问二维数组中元素的方式为根据上述公式计算出元素内存地址,而后使用MOV命令读取该地址的值
给出汇编代码访问A[i][j]的方式:
该算法将12i这个乘法表达式拆成多个加法表达式,这是利用了第二章乘以常数这一节的特性
根据上图定义的定长二维数组(矩阵),给出下面矩阵乘法的算法:
每次计算矩阵元素时,都得使用上一小节给出的公式,GCC会针对这种状况进行优化,减小对公式的直接使用,转而依赖指针运算
int A[n1][n2]
,容许n一、n2是表达式,编译期没法得知该数组的大小。该功能在C99才引入
声明变长数组时,须要将参数n放到参数A[n][n]以前
上图为汇编代码访问A[n][n]数组中,A[i][j]元素的方式。相比定长数组中,因为编译期间没法知晓n的值,没法对4(n*j)
进行优化(拆分红多个加法)
上图为GCC对变长数组的矩阵相乘算法的优化,并无给出汇编代码。跟定长数组中的优化同样,避免使用公式,转而采用指针运算(变长数组直接使用公式的效果更差,由于不知道n的大小,没法对4(n*j)
进行优化)
假设struct rec*类型的变量r存放在寄存器%rdi,编译器会将r->j=r->i
翻译成:
更复杂的实例:
综上,结构的各字段在编译时会转换成内存地址
结构和联合:
在内存占用上的区别:
上图中展现的占用量包含了数据对齐,这部分下一节介绍
相比结构,联合在存储多个字段的时候占用内存更小。但改善空间不大的时候,使用联合绕过了C语言类型系统,容易致使bug产生
实例:访问不一样数据类型的位模式
强转生成的u,值与d同样,可是二者位表示大相径庭(0.0除外)
使用联合生成的u,位模式跟d如出一辙,可是值不一样(0.0除外)。参考java库中的Double.doubleToRawLongBits
实例:分段表示位模式,须要注意机器字节顺序
为了简化访问基本数据类型的方式,一般须要让基本数据类型的地址是其字节大小的整数倍。
针对结构:
不对齐:
对齐:
存在溢出风险的程序:
该程序的栈空间为:
若是用户输入的字节超过24字节,就会破坏调用者的栈帧。根据用户输入的字节数,程序会受到以下破坏:
这个破坏容易致使程序受到攻击。具体方式为:
这样程序的意图就遭到了篡改
对抗溢出攻击的方式有两种:
以前的机器代码,在编译期就能肯定须要为栈帧分配多少空间。有些函数(如alloca)在运行期才会在栈上(malloc是在堆上)分配空间
上图代码是一个实例:存在变长数组,编译期没法肯定数组分配空间(n未知),因此这部分空间须要运行时分配,可是分配后该如何释放这部分空间,并正确的返回到返回地址所在的位置?
x86-64采用寄存器%rbp做为帧指针(帧指针只会在实现变长栈帧时才会用),用来保存动态分配以前栈指针的位置,在函数返回前经过leave指令释放栈指针和恢复帧指针
leave等价于:
介绍了程序计数器、整数寄存器、条件码寄存器,如今只有向量寄存器没有介绍,这部分关联了浮点数的机器级操做方式
处理器定义了浮点体系结构,关系着浮点数据操做如何映射到机器上,这个结构包含:
本文档基于AVX2,在Core i7 Haswell处理器引入。该体系结构容许数据存在16个YMM寄存器,在对标量数据操做时,这些寄存器只保存浮点数,并只是用低32位(float)或64位(double)
传送:
传送实例:
其中都是对float类型数据进行赋值(传递)。之因此出现%rdi和%rsi这两个整数寄存器,是由于它们记录了某个浮点数的内存地址,这个内存地址是整数
转换:
下面两个代码是GCC在单精度和双精度作转换所生成的,意图不明:
结合浮点体系结构一节图示,XMM寄存器就能够处理向函数传递浮点参数,以及从函数返回浮点值
实例:
经过实例观察到:
因此浮点运算中,编译器须要为常量提早分配内存空间,以后将浮点指令会使用内存地址做为操做符,来使用常量
上图,编译器在标号为.LC2和.LC3的内存地址存储了常量1.8和32.0
上述浮点比较指令会设置三个条件码:
无序状况就是两个操做数中存在NAN的状况,这是会将PF设为1
条件码的设置条件为: