CSAPP第三章——程序的机器级表示:学习笔记总结

1,050 阅读14分钟

花了半个多月,补完王爽老师的汇编语言后,跟着CMU的视频课+课本,学完了第三章的知识,最深的感触就是CSAPP无论是视频还是书的质量都非常的硬,不愧它的盛名。(lab6和课后的家庭作业还没做,之后再补) 现在来对前面所学做一个总结。(大致按照CMU视频的顺序进行)

一、Basics 基础

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 movq

al:1字节 ax:2字节 eax:4字节 rax:8字节

3.数据传送 传内存数据的格式:在这里插入图片描述 传地址的格式: 在这里插入图片描述 扩展: 扩展分为零扩展和符号扩展,若要扩展后保持相同的数,对于有符号数,符号扩展(即高位补原来的最高位)可以保证扩展前后相同,对于无符号数,零扩展可以保证扩展前后相同。指令,以字节->字为例,其他同样格式。 零扩展:movzbw dl,ax 符号扩展:movsbw dl,ax 另,符号扩展多一个cltq指令,表示将%(eax)符号扩展->rax 另,当给32位寄存器赋值时,总会将高位自动改为0

4.寄存器作用

0-630-310-158-150-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.

imulq:有符号全乘法 mulq:无符号全乘法 【上面两条指令都需要一个参数在%rax中】 idivq:有符号除法 divq:无符号除法 乘积存在%rdx(高64位)和%rax(低64位) 商存在%rax中,余数存在%rdx中。

question:为什么家庭作业1中的imulq指令乘积存在rdx中...??

二、Control 控制

1.Condition code 条件码

条件码英文含义
CFCarry Flag (for unsigned)无符号数相加时的进位标记
ZFZero Flag结果是0
SFSign Flag (for signed)符号标记,运算结果最高有效位为1(负数),则SF置为1
OFOverflow 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语句 在这里插入图片描述

三、Procedures 过程

栈比较特别,他是自高地址往下生长的。 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前先存起来【自己的东西自己保护好】

今天先到这里,明天再续。

四、Data 数据

内容:

五、Advanced topics

内容: