花了半个多月,补完王爽老师的汇编语言后,跟着CMU的视频课+课本,学完了第三章的知识,最深的感触就是CSAPP不管是视频仍是书的质量都很是的硬,不愧它的盛名。(lab6和课后的家庭做业还没作,以后再补) 如今来对前面所学作一个总结。(大体按照CMU视频的顺序进行)java
1.使用指令新建、编辑、汇编、连接汇编语言程序 ①新建并编辑源代码 命令:getdit sum.c 说明:gedit是一个GNOME桌面环境下兼容UTF-8的文本编辑器。使用vi或者vim一样能够实现新建与编辑。 ②预处理【sum.c -> sum.i】 命令:gcc -E sum.c -o sum.i 【sum.c -> sum.i】 说明:预处理时,编译器会将C源代码中包含的的头文件编译进来 ③编译 【sum.i -> sum.s】 命令:gcc -S sum.i -o sum.s 说明:gcc首先检查代码的规范性,是否有语法错误,肯定代码实际要作的工做,让后将代码翻译成汇编语言 ④汇编【sum.s -> sum.o】 命令:gcc -c sum.s -o sum.o 说明:gcc进行汇编阶段,将编译阶段生成的”.s”文件转成二进制目标代码(可重定位目标文件) ⑤连接【sum.o -> sum】 命令:gcc sum.o -o sum 说明:连接过程将有关的目标文件彼此链接起来,使得全部目标文件成为一个可以执行的统一总体。 ⑥执行 命令:./sum 说明:执行可执行文件,输出结果算法
2.数据格式 b(byte):字节 w(word):1字=2字节 l(double words):双字=4字节 q(quad words):四字=8字节 对应的mov指令为: movb movw movl movqvim
al:1字节 ax:2字节 eax:4字节 rax:8字节c#
3.数据传送 传内存数据的格式:
传地址的格式:
扩展:
扩展分为零扩展和符号扩展,若要扩展后保持相同的数,对于有符号数,符号扩展(即高位补原来的最高位)能够保证扩展先后相同,对于无符号数,零扩展能够保证扩展先后相同。指令,以字节->字为例,其余一样格式。 零扩展:movzbw dl,ax 符号扩展:movsbw dl,ax 另,符号扩展多一个cltq指令,表示将%(eax)符号扩展->rax 另,当给32位寄存器赋值时,总会将高位自动改成0数组
4.寄存器做用性能优化
0-63 | 0-31 | 0-15 | 8-15 | 0-7 | 使用惯例 |
---|---|---|---|---|---|
%rax | %eax | %ax | %ah | %al | 保存返回值 |
%rbx | %ebx | %bx | %bh | %bl | 被调用者保存 |
%rcx |
%ecx | %cx | %ch | %cl | 第4个参数 |
%rdx |
%edx | %dx | %dh | %dl | 第3个参数 |
%rsi |
%esi | %si | 无 | %sil | 第2个参数 |
%rdi |
%edi | %di | 无 | %dil | 第1个参数 |
%rbp | %ebp | %bp | 无 | %bpl | 被调用者保存 |
%rsp | %esp | %sp | 无 | %spl | 栈指针 |
%r8 |
%r8d | %r8w | 无 | %r8b | 第5个参数 |
%r9 |
%r9d | %r9w | 无 | %r9b | 第6个参数 |
%r10 | %r10d | %r10w | 无 | %r10b | 调用者保存 |
%r11 | %r11d | %r11w | 无 | %r11b | 调用者保存 |
%r12 | %r12d | %r12w | 无 | %r12b | 被调用者保存 |
%r13 | %r13d | %r13w | 无 | %r13b | 被调用者保存 |
%r14 | %r14d | %r14w | 无 | %r14b | 被调用者保存 |
%r15 | %r15d | %r15w | 无 | %r15b | 被调用者保存 |
前六个参数分别存在 rdi rsi rdx rcx r8 r9 中,如有更多的参数,则放在栈上,且栈顶为第7个参数,其次第8个参数... |
|||||
rbx rdx r12~r15 是被调用者保存,在函数体中若是要调用别的函数,能够把有用的数据放在这些被调用者保存的寄存器中,这样在被调用的函数中,若是要改变这些寄存器的值,老是会事先入栈保存并在return前弹出。 |
|||||
而调用者保存的寄存器,在调用函数前,不但愿被修改的有用数据老是要先保存在栈中,在调用函数后再弹出使用。 |
5.算术和逻辑操做 注意算术右移(SAR)和逻辑右移(SHR),算术右移是有符号数的右移,高位补1,逻辑右移是无符号数的右移,高位补0.markdown
imulq:有符号全乘法 mulq:无符号全乘法 【上面两条指令都须要一个参数在%rax中】 idivq:有符号除法 divq:无符号除法 乘积存在%rdx(高64位)和%rax(低64位) 商存在%rax中,余数存在%rdx中。less
question:为何家庭做业1中的imulq指令乘积存在rdx中...??
编辑器
1.Condition code 条件码函数
条件码 | 英文 | 含义 |
---|---|---|
CF | Carry Flag (for unsigned) | 无符号数相加时的进位标记 |
ZF | Zero Flag | 结果是0 |
SF | Sign Flag (for signed) | 符号标记,运算结果最高有效位为1(负数),则SF置为1 |
OF | Overflow Flag (for signed) | 溢出标记,表示有符号数的溢出(两个同符号数相加结果为不一样符号,就会有这个溢出) |
![]() |
||
lea不会设置这四个标志位。 | ||
cmpq用于比较大小,testq用于将某个数和0比较 |
CF和OF的区别:
(参考了大佬的blog) ①首先须要知道,计算机对数值的存储采用补码形式存储,一来避免了+0和-0的尴尬,二来数值的加法和减法能够统一为补码的加法。在汇编语言层面,定义变量的时候,没有 signed和unsignde 之分,汇编器通通将你输入的整数字面量看成有符号数(最高位的符号位根据输入的数值符号决定)处理成二进制补码存入到计算机中,只有这一个标准!汇编器不会区分有符号仍是无符号而后用两个标准来处理,它通通看成有符号的!而且通通汇编成补码!
②那么,有符号和无符号数在计算机中是怎么区分并对他们的运算采用不一样的策略呢?有一个重要的点是,补码是一个强大的设计,其统一了无符号数和有符号数的加法运算是相同的,即从0位到高位一个个相加,且相加的时候再加上从前面的进位。【因此用同一个加法器便可】那么,无符号数和有符号数在加法运算的时候并不区分,用同一个加法器便可
,只是对结果拥有不一样的解释权罢了【可是乘法运算用不了同一套了,能力有限】
③ OF、CF、SF标志。 先看CF标志位,书上说CF标志位只对无符号数有意义,首先明白一点,即便是两个有符号数相加,也会致使CF的变更,并非说有符号数,编译器不设置CF位
。由于CF的标志位的变更是因为最高有效位(若是对于8位数,就是第8位)向更高位(第9位)产生了进位或者借位而产生,而对于有符号数来讲,最高位是符号位,它的变更和数值位的变更意义不同。因此对于有符号数,CF也可能发生变更,可是它的变更是没意义的。而若是是无符号数,它的变更就意味中8位的内存或寄存器不足以保存数据,由于数据产生了进位或借位。
再看OF标志位,它只对有符号数有意义,由于两个标准的8位有符号数据(标准指的是赋值的时候不要赋超过有符号数范围的数字,因为截断,便是8位能保存,保存进来的数据数值大小早就产生了变化),这2个数据只有同号(都为正或为负)相加才会溢出,也就是结果超过有符号数的范围
。例如2个正数,符号位(第8位)都为0,相加后发生溢出,符号位因为第7位的进位变成了1,两个正数相加变为了负数?由此对OF产生了做用,如此来讲OF的做用是因为符号位发生变化,若是是两个无符号数,最高位表明的并非符号意义,产生了变更也是无心义的,因此说OF只对有符号数有意义。 最后SF标志,有了上面的介绍,就能理解SF看的是最高位的符号位意义,对于无符号数来讲,最高位表明的是数值意义,并非符号意义。
④可爱又可怕的c语言。 为何又扯到 c 了?由于大多数遇到有符号仍是无符号问题的朋友,都是c里面的 signed 和 unsigned 声明引发的,那为何开头是从汇编讲起呢?由于咱们如今用的c编译器,都是将c语言代码编译成汇编语言代码,而后再用汇编器汇编成机器码的。搞清楚了汇编,就至关于从根本上明白了c,并且,用机器的思惟去考虑问题,必须用汇编。(我通常遇到什么奇怪的c语言的问题都是把它编译成汇编来看。) C 是可爱的,由于c符合kiss 原则,对机器的抽象程度刚恰好,让咱们即提升了思惟层面(比汇编的机器层面人性化多了),又不至于离机器太远 (像c# ,Java之类就太远了)。当初K&R 版的c就是高级一点的汇编……:-) C又是可怕的,由于它把机器层面的全部的东西都反应了出来,像这个有没有符号的问题就是一例(java就不存在这个问题,由于它被设计成全部的整数都是有符号的)。为了说明c的可怕特举一例:
#include <stdio.h>
#include <string.h>
int main() {
int x = 2;
char * str = "abcd";
int y = (x - strlen(str) ) / 2;
//注:原做者这样写,编译器可能会对其优化,直接使用右移移位指令而不是采用除法指令,改为3便可看到
printf("%d\n",y);
}
复制代码
结果应该是 -1 可是却获得:2147483647 。为何?由于strlen的返回值,类型是size_t,也就是unsigned int ,与 int 混合计算时,int类型被自动转换为unsigned int了,结果天然出乎意料。。。 观察编译后的代码,除法指令为 div ,意味无符号除法。解决办法就是强制转换,变成 int y = (int)(x - strlen(str) ) / 2; 强制向有符号方向转换(编译器默认正好相反),这样一来,除法指令编译成 idiv 了。咱们知道,就是一样状态的两个内存单位,用有符号处理指令 imul ,idiv 等获得的结果,与用 无符号处理指令mul,div等获得的结果,是大相径庭的!因此牵扯到有符号无符号计算的问题,特别是存在讨厌的自动转换时,要倍加当心!(这里自动转换时,不管gcc仍是cl都不提示!!!) 为了不这些错误,建议,凡是在运算的时候,确保你的变量都是 signed 的。
2.Conditional branches 条件分支 注:greater和less是有符号的,above和below是无符号的。
注:原则:老是先判断!test而后决定是否转向else
(上面这个是条件控制转移)
Conditional move条件传送
注:这是一种分支预测优化技术,基本思想是把then代码和else都执行获得两个结果,而后才会选择使用哪个结果,看起来彷佛浪费时间但事实是若是是简单的计算,会更有效率,学到性能优化时会明白缘由。【这是一种流水线技术,当代码运行时到达一个分支,他们会试着猜想分支结果,这被称为分支预测技术,而且他们很是擅于预测,98%的时候他们都能猜对,因此他们能够在路上预测suta曲线,并开始朝这个方向前进,只要猜想正确,就会很是有效率,可是若是分支猜错了,必需要阻止它并转向另外一个方向从新开始】
3.Loops 循环 do while:
while:
for:
4.switch语句 Switch语句利用了Jump Table跳转表机制,跳转表机制避免了须要顺序地判断各个case(O[n])【或者二分算法O(logn)】,而能够根据偏移直接跳转到那个代码块case(O(1))
注:switch语句老是用ja (max of x) 先判断是否为默认,如果默认的话直接跳转默认,不是默认的话才利用跳转表机制,以x的值为索引去查跳转表,而后跳到那个跳转表中存着的分支代码的地址。
若是不是从0开始的,那么会给一个偏移量【因此总变成从0开始因此】
若是case值很稀疏,那么编译器会优化成if-else语句
栈比较特别,他是自高地址往下生长的。 callq:
1.下条指令的地址入栈 2.rip转到函数调用的入口处 retq:
(假定栈顶是想要跳转的地址): push rip 传递数据:
rdi rsi rdx rcx r8 r9传递数据,多余的在栈中传递(第7个在栈顶)。 这里的参数要求是整形或指针浮点类型的参数是由另一组单独的寄存器来传递的. 运行方式:
若是是单线程的运行模型Single threaded model,在函数嵌套调用中,会不断地往压栈,在任什么时候刻只有一个函数在运行,在函数返回时,直接能够根据栈顶的地址返回。因此能够不断地调用函数和返回函数来实现复杂的工做。 栈帧:(不肯定对错)
栈帧是从刚进入这个函数后,return前栈多出的那部分,包括了在该函数中的局部变量,函数再调用其余函数的返回地址及参数超过6时的传参 以及其余。<也就是控制权在该函数时所分配的全部栈内存> 需知但不须要理解:
咱们一般能够看到,程序常常在栈上分配比实际需求多的空间,这是由于,有一些约定要求内存地址保持对齐,对齐的方式能够有不少种,这有点含糊不清,但不用担忧是否会有未使用的空间和函数(目前不须要理解).<或许其中一个缘由是canary金丝雀> rsp与rbp:
在栈中,咱们须要的只是在栈中为每一个被调用且未返回的过程保留一个栈帧。一般一个栈帧由两个指针分隔,一是栈指针RSP,一是基指针RBP,但基指针是一个可选项,通常不会使用除了很是特殊的状况,因此这个寄存器实际上并不会在你的程序中以帧指针的形式出现,它将被用做常规寄存器。
若是是分配固定栈帧的,如这里分配16,那么在结尾编译器会释放掉这个空间。可是若是是可变长的数组或内存缓冲区
时,编译器不知道分配多少空间,那么就会采用基指针来记录一开始的栈帧,而后在结束后返回时令rps=基指针
被调用者保存:
(rbp,rbx,r12~r13),例如A调用B,若是B可能会改变A中的这些寄存器的话,那么在B中会先入栈。【帮调用者擦屁股】
这里有一点困惑,之因此常函数或者常量参数尽可能加const的缘由,应该就是为了能让编译器知道是不会改变的,就免去了保存的过程,吗???
调用者保存:
(除了上面那些寄存器)例如A调用B,若是调用B可能会改变这些寄存器的值,那就先在A调用B前先存起来【本身的东西本身保护好】
今天先到这里,明天再续。
内容:
内容: