2017-2018-1 学号20155209 《信息安全系统设计基础》第十三周学习总结
重新深入学习第三章内容
程序的机器级表示
- 概述: 这一章主要是介绍高级语言,例如C语言编写的程序,经过编译后转换为汇编程序。不论我们是在用C语言还是用JAVA或是其他的语言编程时,我们会被屏蔽了程序的机器级的实现。机器语言不需要被编译,可以直接被CPU执行,其执行速度十分快。但是机器语言的读写性与移植性较高级语言低。高级语言被编译后便成为了汇编语言,汇编语言十分接近机器语言。之后汇编代码会转化为机器语言。虽然现代的编译器能帮助我们将高级语言转化为汇编语言,解决了不少问题,但是对于我们正在学习理解计算机系统的学生来说,需要做到能够阅读和理解汇编语言。
3.1历史观点
内容总结
- Intel处理器系列俗称x86,开始时是第一代单芯片、16位微处理器之一。
- 每个后继处理器的设计都是后向兼容的——较早版本上编译的代码可以在较新的处理器上运行。
- X86 寻址方式经历三代:1、DOS时代的平坦模式,不区分用户空间和内核空间,很不安全。2、8086的分段模式。3、IA32的带保护模式的平坦模式。
- ISA:指令集体系结构,机器级程序的指令和格式。它定义了处理状态,指令的格式,以及每条指令对状态的影响。
- IA32 指令长度从1到15个字节不等。常用的指令以及操作数较少的指令所需的字节数少,而那些不太常用或者操作数较多的指令所需的字节数较多。
3.2 程序编码
内容总结
-
一个C语言程序需要经过四个阶段才能变成一个可执行的二进制代码。
-
预处理阶段:预处理器cpp根据编译文件以“#”开头的命令,读取系统头文件stdio.h(.h结尾的表示头文件,.c表示可执行文件)的内容,并把它插入到程序文本中,得到一个新的文件。
-
编译阶段:编译器ccl将预处理后的文件翻译成.s结尾的文本文件,里面包含一个汇编程序。(linux命令:gcc -Og -s hello.c)
-
汇编阶段:汇编器ss将汇编程序翻译成二进制的机器语言,并把结果保存在以.o结尾的二进制文件中。(linux命令:gcc -Og -c hello.c)
-
链接阶段:链接器ld将程序用到的C语言类库的函数汇编后的代码合并到hello.o,得到可执行的目标文件。(linux命令:gcc -o hello hello.c)
-
其中重点:对二进制文件进行反编译:objdump -d hello.o
我的问题及理解
- 我对本节内容的问题:gcc产生的汇编代码怎样去查看和阅读。
- 解答:
- 先写一个c程序。使用gcc -S编译.c文件,会产生.s文件,打开.s文件即可看到汇编代码。
- 使用gcc生成目标代码文件,gcc -c .c文件,在当前目录下会产生.o文件的二进制代码,如果要打开这个文件,可以使用反汇编器,objdump -d .o文件,即可查看。
- GCC产生的汇编代码有点难读,它包含一些我们不关心的信息。所有以 "." 开头的行都是指导汇编器和链接器的命令,称为“汇编器命令”。
3.3数据格式
内容总结
- Inter使用术语“字(word)”表示16位数据类型,因此32位数为“双字”,64位数为“四字”。
- 大多数GCC生成的汇编代码指令都有一个字符的后缀,表明操作数的大小。例如数据传送指令有五个变种:movb=传送字节、movw=传送字、movl=传送双子、movq=传送四字、movbsq=传送绝对的四字。
3.4访问信息
内容总结
-
最初的8086中有8个16位的寄存器,即上图的%ax到%bp。扩展到IA32架构时,这些寄存器也扩展到32为位寄存器,标号从%eax到%ebp。扩展到x86-64后,原来的8位寄存器扩展到64位,标号从%rax到%rbp。除此之外还增加了8个新的寄存器,命名为%r8到%r15。
-
在再常见的程序里不同的寄存器扮演着不同的角色。其中最重要的是栈指针%rsp,用来指明运行时栈的结束位置。
-
这是IA32中央处理器所包含的一组八个存储单元的32位存储器。前六个是通用寄存器,对它们的使用没有限制。前三个寄存器(%eax,%ecx,%edx)的保存和恢复惯例不同于接下来的三个寄存器(%ebx,%esi,%edi)。最后两个寄存器保存着指向程序栈重要位置的指针,称为栈指针和帧指针。数据存放在寄存器中进行加减乘除等一些操作。原来的寄存器是16位的所以如图所示蓝色部分是0-15,之后寄存器进行了扩充,变成了32位的即0-31。
-
操作数指示符
-
大多数指令有一个或者多个操作数,指示该操作的元数据,以及放置目标的位置。x86-64支持多种操作数格式,源数据可以以常数形式给出,或是从寄存器或者内存中读出。根据读出位置的不同操作数的寻址大致分为三种形式。
-
立即数寻址:用来表示常数。在ATT格式的汇编代码中,立即数的表示方式为‘$’后面跟一个标准C语言表示的整数。
-
寄存器寻址:表示某个寄存器的内容,汇编中使用%+寄存器表示。
-
内存引用:根据计算出来的地址访问某个内存地址。
-
数据传送指令
-
数据传送指令:将数据从一个位置复制到另一个位置的指令。
- S表示源操作指定的值是一个立即数,存储在寄存器中或者内存中。
- D表示目的操作数指定一个位置,要么是一个寄存器或者是一个内存地址。x86-64加入了一条限制,传送指令两个操作数不能都指向内存位置。
- movb、movsbl和movzbl之间的差别如下:
假设 %dh =8D,%eax=98765432
movb %dh ,%a1 %eax=9876548D
movsbl %dh , %eax %eax=FFFFFF8D
movzbl %dh,%eax %eax=0000008D
-
在以上的例子中都是将寄存器%eax 的低位字节设置为%edx 的第二个字节。movb指令不改变其他三个字节。根据原字节的最高为,movsbl指令将其他三个字节设为全1或全0。movzbl指令无论如何都是将其他三个字节设为全0。
-
pushl 与popl是用来将数据压入栈中和从栈中弹出数据的。它们两个的指令都只有一个操作数,即它们所要压入或者弹出的数据。将一个双字值压入栈中,首先要将栈指针减4,然后将值写入到新的栈顶地址。弹出一个双字的操作是从栈顶位置读出数据,然后将栈指针加4。
-
压入与弹出栈数据
-
栈是一种数据结构,可以添加和删除数据,不过要遵循“后进先出”的原则,通过push操作将数据压入栈中,通过pop操作删除栈中数据。栈可以实现为一个数组,总是从栈的一端插入和删除元素,这一端称为栈顶。在x86-64中,程序栈存放在内存中的某个位置。
-
在内存中栈顶元素的地址是所有栈中元素地址中最低的。(按照惯例,我们的栈是倒过来画的,栈顶在底部。)栈指针%rsp保存着栈顶元素的地址。
-
上图中,开始%rsp = 0x108,%rax = 0x123。执行pushq %rax的效果,首先%rsp会减8,得到0x100,然后会将0x123存放到内存地址0x100处。
-
其中push指令相当于这两条指令subq $8 %rsp,mov %rbp %rsp;
我的问题及理解
- 一个程序在运行过程中,一个函数会调用另一个函数(比如递归),那么函数在进入的时候都会往栈里压点什么东西,函数退出时会弹出点什么东西,内层的函数是如何返回的,返回给外层函数的谁,返回到哪里,内层函数是怎么知道返回地址的?
- 解答:
- 会压入: 原来ebp的值, 参数, 以及调用函数的下一个指令地址
在调用一个函数时, 编译器就计算好函数需要的空间, 然后esp = ebp-需要的空间, 通过ebp+偏移量来访问。
在函数里调用另外一个函数时, 原来fun的ebp值压栈。 - 退出时会做函数调用时的逆操作,内层函数是通过之前已经压栈了的调用函数时的下一条指令来得知返回地址的。
- 参数和返回地址按照某个约定顺序压入栈中。然后跳到目标代码。执行完之后。按照约定把这些信息清理出去,或者由调用方清理,看约定。把返回值放在寄存器或者栈中。恢复原来的位置,继续执行。所以需要保留的现场信息全部在栈里面。
算术和逻辑操作
内容总结
-
如图所示共有四种不同的操作:leal 、一元操作、二元操作、位移操作。
-
加载有效地址
-
加载有效地址(load effective address)指令leal实际上是movl的变形。它的指令形式是从存储器读数据到寄存器,但实际上没有引用存储器。它的第一个操作数看上去是一个存储器引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。
-
如果寄存器%eax的值为x,那么指令leal 7(%edx, %edx, 4),%eax将设置寄存器%eax的值为5x+7.
-
一元操作和二元操作
-
一元操作:只有一个操作数,既是源又是目的,这个操作数可以是一个寄存器,也可以是一个存储器位置。
-
指令incl(%esp)会使栈顶的4字节元素加1,与C语言的自增,自减操作符有点类似。
-
二元操作:第二个操作数既是源又是目的,与C语言的赋值运算符类似,例如x+=y;不过,要注意,源操作数是第一个,目的操作数是第二个。
-
指令subl %eax, %edx :使寄存器%edx的值减去%eax的值。
-
位移操作
-
位移操作,先给出位移量,然后是待移位的值。可以进行算术和逻辑右移。
-
移位量可以是一个立即数,或者放在单字节寄存器元素%cl中。
-
左移指令有两个名字:SAL和SHL,两者效果一样,都是将右边填上0;右移指令不同,SAR执行算术移位(填上符号位),而SHR执行逻辑移位(填上0),移位操作的目的操作数可以是一个寄存器或者是一个存储器位置。
-
特殊的算术操作
-
imull指令:“双操作数”乘法指令,从两个32位操作数产生一个32位乘积。(补码乘法)
-
mull指令:无符号数乘法
-
这两个指令都要求一个参数必须在寄存器%eax中,而另一个作为指令的源操作数给出,然后乘机存放在寄存器%edx(高32位)和%eax(低32位)中。
-
idivl:有符号除法指令将寄存器%edx(高32位)和%eax(低32位)中的64位数作为被除数,而除数作为指令的操作数给出。指令将商存储在寄存器%eax中,将余数存储于寄存器%edx中。
我的问题及理解
-
什么是算术运算?什么是关系运算?什么是逻辑运算?
-
解答
-
算术运算、关系运算和逻辑运算是计算机运算中的基本运算方式。
-
算术运算是基本的数值运算,在C语言中有加、减、乘、除和除余五种。另外还有单项算术运算和前置运算和后置运算等变化。输出值还是数值。
-
关系运算主要是对两个 运算量进行大小 关系的比较,输入值为1或0两个逻辑值。
-
逻辑运算是表示运算量的逻辑关系,运算的结果也是1或者0.
-
各种运算在同一个算式中也有顺序问题,C语言的运算顺序比较复杂,有15个优先级。
控制
内容总结
-
条件码
-
条件码(condition code)寄存器。它与整数寄存器不同,它是由单个位组成的寄存器,也就是它们当中的值只能为 0 或者 1。当有算术与逻辑操作发生时,这些条件码寄存器当中的值会相应的发生变化。
-
整数寄存器,在 32位 CPU 中包含一组 8 个存储 32 位值的寄存器,即整数寄存器。它可以存储一些地址或者整数的数据,有的用来记录某些重要的程序状态,有的则用来保存临时数据。
-
常用的条件码如下:
-
CF:进位标志寄存器。最近的操作是最高位产生了进位。它可以记录无符号操作的溢出,当溢出时会被设为1。
-
ZF:零标志寄存器,最近的操作得出的结果为0。当计算结果为0时将会被设为1。
-
SF:符号标志寄存器,最近的操作得到的结果为负数。当计算结果为负数时会被设为1。
-
OF:溢出标志寄存器,最近的操作导致一个补码溢出(正溢出或负溢出)。当计算结果导致了补码溢出时,会被设为1。
-
除了上面的算数逻辑运算可以设置条件码,还有两类指令会设置条件码,并且不更新目的寄存器,它们分别是CMP和TEST。CMP和SUB指令相似,TEST和AND指令相似。
-
CMP 指令,指令形式 CMP S2,S1。然后会根据 S1-S2 的差来设置条件码。除了只设置条件码而不更新目标寄存器外,CMP 指令和 SUB 指令的行为是一样的。比如两个操作数相等,那么之差为0,那么就会将零标志设置为 1;其他的标志也可以用来确定两个数的大小关系。
-
TEST 指令,和 AND 指令一样,除了TEST指令只设置条件码而不改变目的寄存器的值。
-
访问条件码
-
对于普通寄存器来讲,使用的时候一般是直接读取它的值,而对于条件码,通常不会直接读取。常用的有如下三种方法:
-
可以根据条件码寄存器的某个组合,将一个字节设置为0或1。
-
可以直接条件跳转到程序的某个其它的部分。
-
可以有条件的传送数据。
-
跳转指令
-
正常情况下,指令会按照他们出现的顺序一条一条地执行。而跳转指令(jump)会导致执行切换到程序中一个全新的位置,我们可以理解为方法或者函数的调用。在汇编代码中,这些跳转的目的地通常用一个标号(label)指明。
movl $0,%eax
jmpl .L1
movl (%eax),%edx
.L1:
popl %edx
- 指令 jmpl .L1 会导致程序跳过 movl 指令,从 popl 开始执行。在产生目标代码文件时,汇编器会确定所有带标号指令的地址,并将跳转目标(目的指令的地址)编码为跳转指令的一部分。
- jump 指令有三种跳转方式:
- 直接跳转:跳转目标是作为指令的一部分编码的,比如上面的直接给一个标号作为跳转目标。
- 间接跳转:跳转目标是从寄存器或者存储器位置中读出的,比如 jmp *%eax 表示用寄存器 %eax 中的值作为跳转目标;再比如 jmp *(%eax) 以 %eax 中的值作为读地址,从存储器中读取跳转目标。
- 其他条件跳转:根据条件码的某个组合,或者跳转,或者继续执行代码序列中的下一条指令。
- 循环
- C 语言提供了多种循环结构,比如 do-while、while和for。汇编中没有相应的指令存在,我们可以用条件测试和跳转指令组合起来实现循环的效果。而大多数汇编器会根据一个循环的do-while 循环形式来产生循环代码,即其他的循环一般也会先转换成 do-while 形式,然后在编译成机器代码。
- 比如
我的问题及理解
-
set指令的各种后缀的意思都是什么?
-
解答
-
e->ZF(相等):equals的意思,这里代表的组合是ZF,因为ZF在结果为0时设为1。因此ZF代表的意义是相等。
-
e->~ZF(不相等):not equals 的意思,这里代表的组合是~ZF,也就是ZF做“非运算”,则很明显是不相等的意思。
-
s->SF(负数):这里代表的组合是SF,因为SF在计算结果为负数时设为1,此时可以认为b为0,即a<0。因此这里是负数的意思。
-
s->~SF(非负数):与s相反,加上n则是not的意思,因此这里代表非负数。
-
l->SFOF(有符号的小于):l代表的是less。这里的组合是SFOF,即对SF和OF做“异或运算”。“异或运算”的意思则是代表,SF和OF不能相等。那么有两种情况,当OF为0时,则代表没有溢出,此时SF必须为1,SF为1则代表结果为负。即a-b<0,也就是a<b,也就是小于的意思。当OF为1时,则代表产生了溢出,而此时SF必须为0,也就是说结果最后为正数,那么此时则是负溢出,也可以得到a-b<0,即a<b。综合前面两种情况,SF^OF则代表小于的意思。
-
e->(SF^OF)|ZF(有符号的小于等于):le是less equals的意思。有了前面小于的基础,这里就很容易理解了。SF^OF代表小于,ZF代表等于,因此两者的“或运算”则代表小于等于。
-
g->(SF^OF)&ZF(有符号的大于):g是greater的意思。这里的组合是(SF^OF)&ZF,相对来说就比较复杂了。不过有了前面的铺垫,这个也非常好理解。SFOF代表小于,则~(SFOF)代表大于等于,而ZF代表不等于,将(SF^OF)与~ZF取“与运算”,则代表大于等于且不等于,也就是大于。
-
ge->~(SF^OF)(有符号的大于等于):ge是greater equals的意思。
-
b->CF(无符号的小于):b是below的意思。CF是无符号溢出标志,这里的意思是指如果a-b结果溢出了,则代表a是小于b的,即a<b。其实这个结论很显然,关键点就在于,无符号减法只有在减出负数的时候才可能溢出,也就是说只要结果溢出了,那么一定有a-b<0。因此这个结论就显而易见了。
-
be->CF|ZF(无符号的小于等于):这里是below equals的意思。因此这里会与ZF计算“或运算”,字面上也很容易理解,即CF(小于)|(或)ZF(等于),也就是小于等于。
-
a->CF&ZF(无符号的大于):a代表的是above。这个组合也是非常好理解的,CF代表小于,则CF代表大于等于,ZF代表不等于,因此CF&ZF则代表大于等于且不等于,即大于。
-
e->~CF(无符号的大于等于):ae是above equals的意思。比如对于setae %al指令来说,%al是%eax寄存器中的最后一个字节,这个指令的含义是,将~CF的值设置到%eax寄存器的最后一个字节。
过程
内容总结
-
过程是软件中一种很重要的抽象。他提供了一种封装代码的方式,用一组指定的参数和一个可选的返回值实现了某种功能。然后,可以在程序中不同的地方调用这个函数。
-
过程机制的构建需要实现下面的一到多个机制。
-
传递控制:在进入过程Q的时候,程序计数器必须被设置为Q代码的起始位置,然后返回时,要把程序程序计数器设置为调用的那一条语句。
-
传递数据:P必须向Q传递n个参数,Q必须向P返回一个值。
-
分配和释放内存:在开始是,Q可能需要为局部空间分配内存,而在返回之前必须释放掉这些存储空间。
-
x86-64的过程实现包括特殊的指令和一些对机器资源使用的约束。
-
运行时的栈
-
当x86-64过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间(栈帧)。下图给出了运行是栈的通用结构,包括划分“栈帧”。当前正在执行的过程的栈帧总是在栈顶。
-
当过程P调用过程Q时,会把返回的地址压入P的栈帧中,指明当Q返回时,P从哪里开始执行。
-
Q的代码会扩展当前栈的边界,分配他的栈帧所需要的空间,在这个空间,它可以保存寄存器的值,分配局部变量的空间,为调用过程设置参数。当Q运行时,P以及所有在向上追溯到P的调用链中的过程都是被挂起的,同时此时Q的栈帧在栈顶。
-
为了提高空间和时间的效率,许多过程有6个或者更少的参数,那么所有参数都保存在寄存器中。
-
转移控制
-
转移控制的实现需要上面两条汇编指令的支持。P调用个过程Q,执行call Q指令,该指令会把调用过程的下一条指令A保存在P的栈帧中,并把PC寄存器设置为Q的起始位置。对应的指令会将PC设置为A,并将A弹出P的栈帧。
-
数据传送
-
当调用一个过程的时候,除了要把控制传递给调用过程,调用还需要把数据作为参数传递过去,调用过程可能返回一个值。
-
大部分数据的传送是通过寄存器来实现的,寄存器最多传输6个小于等于64位的数据,并通过%rax返回数据。
-
如果一个函数有大于6个整型参数,超出6个的部分就通过保存在调用者的栈帧来传递。
-
上面的程序代码,前六个参数可以通过寄存器传递,后面的两个通过栈传递。
-
栈上的局部存储
-
目前为止我们看到的大多数程序示例都不需要超过寄存器大小的本地存储。不过以下情况局部数据必须要放入内存中。
-
寄存器不足以存放所有的本地数据。
-
对一个局部变量使用运算符“&”。
-
某些局部变量是数组或者是结构体,必须能够通过数据的引用访问到。
-
寄存器的局部存储空间
-
寄存器是唯一在所有过程中共享的资源。经过函数的调用,可能会改变参数寄存器里面的值,当函数调用结束后让,调用函数使用改变后的寄存器的值是不正确的,所以调用的函数采用了这种机制,就是将寄存器的值先保存在,调用者的栈帧中,在被调用者返回前,会通过栈帧里的数据回复寄存器里面的值。
-
递归过程
-
因为寄存器和栈帧的存在是的x86-64过程能够递归的调用自身,每个过程调用在栈中都有自己的私有空间,因此多个未完成的调用的局部空间不会相互影响,栈的原则也提供了适当的策略,当过程被调用时分配局部存储,返回时释放局部存储。
-
递归过程
-
本章对于函数的汇编实现做了详细的讲解,主要是栈规则的机制,帮我们解决了数据如何在调用者和被调用者之间传递,以及在被调用者当中局部变量内存的分配以及释放。
我的问题及理解
- 数据段、代码段、堆栈段、BSS段的区别
- 解答
- BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。
- 数据段:数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
- 代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
- 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
- 栈(stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变 量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
- 参考资料
数组分配和访问
内容总结
- 在编写程序的时候,不同数据类型进行转换和计算的时候总是可能出现各种各样奇怪的bug,所以深入的了解计算机系统中数据的表示与存储有利于我们编写高效与健壮的计算机程序。
- 基本原则
- 我们知道数组是某种基本数据类型数据的集合,对于数据类型 T 和整型常数 N,数组的声明如下:
T A[N]
-
上面的 A 称为数组名称。它有两个效果:
-
它在存储器中分配一个 L*N 字节的连续区域,这里 L 是数据类型 T 的大小(单位为字节)
-
A 作为指向数组开头的指针,如果分配的连续区域的起始地址为 xa,那么这个指针的值就是xa。
-
即当我们用 A[i] 去读取数组元素的时候,其实我们访问的是 xa+i*sizeof(T)。sizeof(T)是获得数据类型T的占用内存大小,以字节为单位,比如如果T为int,那么sizeof(int)就是4。因为数组的下标是从0开始的,当 i等于0时,我们访问的地址就是 xa。
-
比如对于如下数组声明:
-
指针运算
-
C语言允许对指针进行运算,而计算出来的值会根据该指针引用的数据类型的大小进行伸缩。也就是说,如果 P 是一个执行类型 T 的数据的指针,P 的值为 xp,那么表达式P+i 的值为 xp+L*i,这里 L 是数据类型T的大小。
-
假设整型数组 E 的起始地址和整数索引 i 分别存放在寄存器 %edx 和 %ecx 中,下面是每个表达式的汇编代码实现,结果存放在 %eax 中。
-
上面例子中,leal 指令用来产生地址,而 movl 用来引用存储器(除了第一种和最后一种情况,前者是复制一个地址,后者是复制索引);最后一个例子说明可以计算同一个数据类型结构中的两个指针之差,结果值是除以数据类型大小后的值。
-
数组的嵌套
-
数组的嵌套,也就是数组的数组,比如二维数组 int A[5][3]。这个时候上面所讲的数组的分配和引用也是成立的。
-
对于数组 int A[5][3],如下表示:
-
我们可以将 A 看成是一个有 5 个元素的数组,而每个元素都是 3 个 int 类型的数组。
-
定长数组和变长数组
-
要理解定长和变长数组,我们必须搞清楚一个概念,就是说这个“定”和“变”是针对什么来说的。在这里我们说,这两个字是针对编译器来说的,也就是说,如果在编译时数组的长度确定,我们就称为定长数组,反之则称为变长数组。
-
比如int A[10],就是一个定长数组,它的长度为10,它的长度在编译时已经确定了,因为长度是一个常量。之前的C编译器不允许在声明数组时,将长度定义为一个变量,而只能是常量,不过当前的C/C++编译器已经开始支持动态数组,但是C++的编译器依然不支持方法参数。另外,C语言还提供了类似malloc和calloc这样的函数动态的分配内存空间,我们可以将返回结果强转为想要的数组类型。
-
对于如下程序:
int main(){
int a[5];
int i,sum;
for(i = 0 ; i < 5; i++){
a[i] = i * 3;
}
for(i = 0 ; i < 5; i++){
sum += a[i];
}
return sum;
}
- 汇编之后:
main:
pushl %ebp
movl %esp, %ebp//到此准备好栈帧
subl $32, %esp//分配32个字节的空间
leal -20(%ebp), %edx//将帧指针减去20赋给%edx寄存器
movl $0, %eax//将%eax设置为0,这里的%eax寄存器是重点
.L2:
movl %eax, (%edx)//将0放入帧指针减去20的位置?
addl $3, %eax//第一次循环时,%eax为3,对于i来说,%eax=(i+1)*3。
addl $4, %edx//将%edx加上4,第一次循环%edx指向帧指针-16的位置
cmpl $15, %eax//比较%eax和15?
jne .L2//如果不相等的话就回到L2
movl -20(%ebp), %eax//下面这五句指令已经出卖了leal指令,很明显从-20到-4,就是数组五个元素存放的地方。下面的就不解释了,直接依次相加然后返回结果。
addl -16(%ebp), %eax
addl -12(%ebp), %eax
addl -8(%ebp), %eax
addl -4(%ebp), %eax
leave
ret
-
循环过程是怎么计算的
-
开始将%ebp减去20是为了依次给数组赋值。这里编译器用了非常变态的优化技巧,那就是编译器发现了a[i+1] = a[i] + 3的规律,因此使用加法(将%eax不断加3)代替了i*3的乘法操作,另外也使用了加法(即地址不断加4,而不使用起始地址加上索引乘以4的方式)代替了数组元素地址计算过程中的乘法操作。而循环条件当中的i<5,也变成了3*i<15,而3*i又等于a[i],因此当整个数组当中循环的索引i,满足a[i+1]=15(注意,在循环内的时候,%eax一直储存着a[i+1]的值,除了刚开始的0)的时候,说明循环该结束了,也就是coml和jne指令所做的事。
我的问题及理解
- 由于娄老师推荐重点是本章节内容。本章学的很认真没有发现什么问题。
异质的数据结构
内容总结
- 异质结构
- 异质结构是指不同数据类型的数组组合,比如C语言当中的结构(struct)与联合(union)。在理解数组的基础上,这两种数据结构都非常好理解。
- 写一个结构的例子
#include <stdio.h>
struct {
int a;
int b;
char c;
} mystruct;
int main(){
printf("%d
",sizeof mystruct);
}
- 这是一个非常简单的结构体,这个程序在32位windows系统上,输出结果是12,其他的还可以得到10或者16这样的结果。
- 这正是因为对齐的原因,这里的对齐不是地址对齐也不是栈分配空间对齐,而是数据对齐。为了提高数据读取的速度,一般情况下会将数据以2的指数倍对齐,具体是2、4、8还是16,得根据具体的硬件设施以及操作系统来决定。
- 这样做的好处是,处理器可以统一的一次性读取4(也可能是其它数值)个字节,而不再需要针对特殊的数据类型读取做特殊处理。在这个例子来说,也就是说在读取a、b、c时,都可以统一的读取4个字节。特殊的,这里0-3的位置用于存储a,4-7的位置用于存储b,8的位置用于存储c,而9-11则用于填充,其中都是空的。
- 与结构体不同的是,联合会复用内存空间,以节省内存,比如:
#include <stdio.h>
union {
int a;
int b;
char c;
} myunion;
int main(){
printf("%d
",sizeof myunion);
}
- 这段程序输出的结果是4,依旧是32位windows操作系统的结果。这是因为a、b、c会共用4个字节,这样做的目的不言而喻,是为了节省内存空间,显然它比结构体节省了8个字节的空间。它与结构体最大的区别就在于,对a、b、c赋值时,联合会覆盖掉之前的赋值,而结构体则不会,结构体可以同时保存a、b、c的值。
- 地址对齐的大致规则,一般会依据数据类型的长度来对齐(比如int为4位对齐,double为8位对齐等等),但最低为2。不过这些都不是绝对的,比如double也可能会依据4位对齐,因此具体的对齐规则还是需要根据硬件设施和操作系统决定。
- 对齐是在拿空间换时间,也就是说,对齐浪费了存储空间,但提高了运行速度。这有点类似于算法的时间复杂度和空间复杂度,两者大部分情况下总是矛盾的。
我的问题及理解
- 各种对齐都改如何理解。地址对齐、数据对齐和栈分配对齐的区别。
- 解答
- 字节对齐,现代计算机中,内存空间按照字节划分,理论上可以从任何起始地址访问任意类型的变量。但实际中在访问特定类型变量时经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序一个接一个地存放,这就是对齐。
- 结构体对齐,在C语言中,结构体是种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构体、联合等)的数据单元。编译器为结构体的每个成员按照其自然边界(alignment)分配空间。各成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
- 栈内存对齐:本章学习中已经了解。
- 位域对齐:有些信息在存储时,并不需要占用一个完整的字节,而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1两种状态,用一位二进位即可。为了节省存储空间和处理简便,C语言提供了一种数据结构,称为“位域”或“位段”。
- 位域是一种特殊的结构成员或联合成员(即只能用在结构或联合中),用于指定该成员在内存存储时所占用的位数,从而在机器内更紧凑地表示数据。每个位域有一个域名,允许在程序中按域名操作对应的位。这样就可用一个字节的二进制位域来表示几个不同的对象。
- 参考资料1
- 参考资料2
在机器级程序中将控制与数据结合起来
内容总结
- 理解指针
- 指针也是变量,不过是与普通的数据类型(int, long, float, double等)不同的变量,因为指针的值记录的是相同类型变量的地址(内存序号)。
- 指针从开始学习c语言开始就一直在接触,我认为不同的人有不同的理解。
- 缓冲区溢出
- 缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量溢出的数据覆盖在合法数据上,理想的情况是程序检查数据长度并不允许输入超过缓冲区长度的字符,但是绝大多数程序都会假设数据长度总是与所分配的储存空间相匹配,这就为缓冲区溢出埋下隐患.操作系统所使用的缓冲区 又被称为"堆栈". 在各个操作进程之间,指令会被临时储存在"堆栈"当中,"堆栈"也会出现缓冲区溢出。
我的问题及理解
- 内存溢出、内存泄露、内存越界、缓冲区溢出、栈溢出的不同和如何分类理解。
- 内存泄漏是指你向系统申请分配内存进行使用(new),可是使用完了以后却不归还(delete),结果你申请到的那块内存你自己也不能再访问(也许你把它的地址给弄丢了),而系统也不能再次将它分配给需要的程序。
- 参考博客
浮点代码
内容总结
- 浮点数
- IEEE (美国电气和电子工程师学会)浮点数
- 计算机中是用有限的连续字节保存浮点数的。
- IEEE定义了多种浮点格式,但最常见的是三种类型:单精度、双精度、扩展双精度,分别适用于不同的计算要求。一般而言,单精度适合一般计算,双精度适合科学计算,扩展双精度适合高精度计算。一个遵循IEEE 754标准的系统必须支持单精度类型(强制类型)、最好也支持双精度类型(推荐类型),至于扩展双精度类型可以随意。单精度(Single Precision)浮点数是32位(即4字节)的,双精度(Double Precision)浮点数是64位(即8字节)的。
- 保存这些浮点数当然必须有特定的格式,Java 平台上的浮点数类型 float 和 double 采纳了 IEEE 754 标准中所定义的单精度 32 位浮点数和双精度 64 位浮点数的格式。
- 在 IEEE 标准中,浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数域三个域,其中保存的值分别用于表示给定二进制浮点数中的符号,指数和尾数。这样,通过尾数和可以调节的指数(所以称为"浮点")就可以表达给定的数值了。
我的问题及理解
- 本章问题都在习题中理解了。
课后作业
1
- 题中给出汇编代码
decode2:
subq %rdx, %rsi # %rsi = y - z;
imulq %rsi, %rdi # %rdi = (y - z) * x;
movq %rsi, %rax # %rax = y - z;
salq $63, %rax # %rax = (y - z) << 63;
sarq $63, %rax # %rax = ((y - z) << 63) >> 63;
xorq %rdi, %rax # %rax=(((y - z) << 63) >> 63) ^ ((y - z) * x)
ret
- 解答:
long decode2(long x, long y, long z)
{
int result = x * (y - z);
if((y - z) & 1)
result = ~result;
return result;
}
2
- 根据题意仔细分析体重给出的代码
store_prod:
movq %rdx, %rax # %rax = y0.
cqto # 有符号运算,因此用cqto,这里会自动关联%rdx和%rax分别表示高位和低位,假如y是负数,那么%rdx所有位都是1(此时值是-1),否则,%rdx全为0, %rdx = y1.
movq %rsi, %rcx # %rcx = x0.
sarq $63, %rcx # 将%rcx向右移63位,跟%rdx的含义一样,要么是-1,要么是0, %rcx = x1.
imulq %rax, %rcx # %rcx = y0 * x1
imulq %rsi, %rdx # %rdx = x0 * y1
addq %rdx, %rcx # %rcx = y0 * x1 + x0 * y1
mulq %rsi # 无符号计算 x0*y0,并将x0*y0的128位结果的高位放在%rdx,低位放在%rax,因此这里%rdx = z1, %rax = z0.
addq %rcx, %rdx # %rdx = y0*x1+x0*y1+z1
movq %rax, (%rdi) # 将%rax的值放到结果的低位
movq %rdx, 8(%rdi)# 将%rdx的值放到结果的高位,可以发现跟上面用数学公式推理的结果完全一致!!!!
ret
- 这个题考察的是2.3.4和2.3.5节的一个定理:w比特长度的两个数相乘,会产生一个2w长度的数,不管这两个数是无符号数还是补码表示的有符号数,把结果截取的低w比特都是相同的。所以我们可以用无符号数乘法指令mulq实现有符号数乘法:先把数有符号扩展致2w位,然后把这两个2w位的数相乘,截取低2w位即可。截取就是求模运算,即 mod 2^w。
3
- 汇编代码
loop:
movl %esi, %ecx # %ecx=n;
movl $1, %edx # %edx=1; --> mask
movl $0, %eax # %eax=0; --> result
jmp .L2
.L3:
movq %rdi, %r8 # %r8=x;
andq %rdx, %r8 # %r8=x&%rdx; -->x&mask
orq %r8, %rax # %rax=%rax | (x&%rdx); -->result |= x & mask
salq %cl, %rdx # %rdx=%rdx<<(n&0xFF); -->mask<<=(n&0xFF)
.L2:
testq %rdx, %rdx
jne .L3. # if %rdx!=0 goto L3. -->mask!=0
rep; ret
- 代码补全如下
long loop(long x, int n)
{
long result = 0;
long mask;
for(mask = 1; mask != 0; mask >>= n)
{
result |= (x & mask);
}
return result;
}
- 回答问题:
- A:%rdi, %r8 --> x
%esi, %ecx --> n
%rdx --> mask
%rax --> result - B:result = 0;
mask = 1; - C:mask != 0
- D:mask<<=(n&0xFF)
- E:result |= x & mask
4
- 传送指令会对条件分别求值,于是假如xp为空指针,那么这里产生对空指针读数据的操作,显然是不可以的。于是这里不能存在*xp,可以用指针来代替,最后判断出值之后,再进行读取数据,因此这里0也必须赋予一个地址,于是需要加个变量来存储0这个数字。因此答案可以是:
long cread_alt(long *xp)
{
int t=0;
int *p = xp ? xp : &t;
return *p;
}
5
- 翻译汇编
case MODE_A:
result = *p2;
action = *p1;
*p2 = action;
break;
case MODE_B:
result = *p1 + *p2;
*p1 = result;
break;
case MODE_C:
*p1 = 59;
result = *p2;
break;
case MODE_D:
result = *p2;
*p1 = result;
result = 27;
break;
case MODE_E:
result = 27;
break;
default:
result = 12;
6
- 先分析过程
<switch_prob>:
400590: 48 83 ee 3c sub $0x3c, %rsi
# 说明下面的数都要加上60
400594: 48 83 fe 05 cmp $0x5, %rsi
400598: 77 29 ja 4005c3 <switch_prob+0x33>
# 如果大于65,跳到4005c3那一行
40059a: ff 24 f5 f8 06 40 00 jmpq *0x4006f8(,%rsi,8)
# 跳到跳转表对应的位置,假设跳转表对应数组a[x],那么分别跳到a[0x4006f8+8*(n-60)]的位置
4005a1: 48 8d 04 fd 00 00 00 lea 0x0(,%rdi,8),%rax
# 60和62会跳到这个位置
4005a8: 00
400593: c3 retq
4005aa: 48 89 f8 mov %rdi, %rax
# 63会跳到这个位置
4005ad: 48 c1 f8 03 sar $0x3, %rax
4005b1: c3 retq
4005b2: 48 89 f8 mov %rdi, %rax
# 64会跳到这个位置
4005b5: 48 c1 e0 04 shl $0x4, %rax
4005b9: 48 29 f8 sub %rdi, %rax
4005bc: 48 89 c7 mov %rax, %rdi
4005bf: 48 0f af ff imul %rdi, %rdi
# 65会跳到这个位置
4005c3: 48 8d 47 4b lea 0x4b(%rdi), %rax
# 大于65和61会跳到这个位置
4005c7: c3 retq
- 根据过程可得答案
long switch_prob(long x, long n){
long result = x;
switch(n):{
case 60:
case 62:
result = x * 8;
break;
case 63:
result = result >> 3;
break;
case 64:
result = (result << 4) - x;
x = result;
case 65:
x = x * x;
case 61: # 也可以去掉这行
default:
result = x + 0x4b;
}
}
7
- 分析过程
store_ele:
leaq (%rsi, %rsi, 2), %rax # %rax = 3 * j
leaq (%rsi, %rax, 4), %rax # %rax = 13 * j
leaq %rdi, %rsi # %rsi = i
salq $6, %rsi # %rsi * = 64
addq %rsi, %rdi # %rdi = 65 * i
addq %rax, %rdi # %rdi = 65 * i + 13 * j
addq %rdi, %rdx # %rdx = 65 * i + 13 * j + k
movq A(, %rdx, 8), %rax # %rax = A + 8 * (65 * i + 13 * j + k)
movq %rax, (%rcx) # *dest = A[65 * i + 13 * j + k]
movl $3640, %eax # sizeof(A) = 3640
ret
- 得到答案
- A:&D[i][j][k] = XD + L(i * S * T + j * T + k)
- B:R = 7 。S = 5 。T = 13。
8
- 分析代码
.L6:
movq (%rdx), %rcx # t1 = A[i][j]
movq (%rax), %rsi # t2 = A[j][i]
movq %rsi, (%rdx) # A[i][j] = t2
movq %rcx, (%rax) # A[j][i] = t1
addq $8, %rdx # &A[i][j] += 8
addq $120, %rax # &A[j][i] += 120
cmpq %rdi, %rax
jne .L6 # if A[j][i] != A[M][M]
- A:从2~5行里无法区分A[i][j]和A[j][i],只能从第6和7行来看,A[i][j]每次只移动一个单位,所以每次+8的寄存器%rdx就是指的A[i][j]。
- B:因为寄存器%rdx是A[i][j],所以另一个寄存器%rax是A[j][i]。
- C:A[j][i]每次移动一行的距离,所以可得公式:8 * M = 120,显然,M=15。
9
- 分析代码
sum_col:
leaq 1(, %rdi, 4), %r8 # %r8 = 4 * n + 1
leaq (%rdi, %rdi, 2), %rax # result = 3 * n
movq %rax, %rdi # %rdi = 3 * n
testq %rax, %rax
jle .L4 # if %rax <= 0, goto L4
salq $3, %r8 # %r8 = 8 * (4 * n + 1)
leaq (%rsi, %rdx, 8), %rcx # %rcx = A[0][j]
movl $0, %eax # result = 0
movl $0, %edx # i = 0
.L3:
addq (%rcx), %rax # result = result + A[i][j]
addq $1, %rdx # i += 1
addq %r8, %rcx # 这里每次+8*(4n+1),说明每一行有4n+1个,因此NC(n)为4*n+1
cmpq %rdi, %rdx
jne .L3 # 这里说明一直循环到3*n才结束,所以可以说明一共有3n行,因此NR(n)为3*n
rep; ret
.L4:
movl $0, %eax
ret
- NR(n) = 3 * n。NC(n) = 4 * n + 1
10
-
A.%rsp+24:z。%rsp+16:&z。%rsp+8:y。%rsp:x。
-
B.传的是%rsp+64表示的栈地址,而不是结构体s.
-
C.直接通过%rsp+偏移量的栈地址来访问的s的值.
-
D.通过所传的表示栈地址的参数,来间接存储在栈上.
-
E.%rsp+80:z。
%rsp+72:x。
%rsp+64:y。
%rsp+32
%rsp+24:z。
%rsp+16:&z。
%rsp+8:y。
%rsp:x。
在从process返回后,eval是通过直接通过访问的%rsp+偏移量来访问的结构r的元素. -
F.在涉及结构体这种无法用一个寄存器存储的参数时,不管是传入还是返回,都是直接通过在栈上的存储来进行访问的.
11
- 代码分析
setVal:
movslq 8(%rsi), %rax
# 说明str2的t从第8位开始的,因为按照8位对齐,因此sizeof(array[B])小于等于8
# 因为下边的t是int类型,只占4个字节,为了不让t与array共占8个字节,所以sizeof(array[B])大于4,因此可得5<=B<=8.
addq 32(%rsi), %rax
# 说明str2的u从第32位开始的,因此t与s占了24个字节,可以将2个s放在t的一行,占满8个字节,剩下的s占据两行,因此可得7<=A<=10.
movq %rax, 184(%rdi)
# 说明str1的y从第184位开始的,因此184-8<A*B*4<=184
- 可求A=9。B=5。
12
- 分析代码
<test>:
mov 0x120(%rsi), %ecx
# 这句话是访问bp的first,说明first与a一共占了288个字节
add (%rsi), %rcx
# %rcx = n
lea (%rdi, %rdi, 4), %rax
# %rax = 5 * i
lea (%rsi, %rax, 8), %rax
# %rax = &bp + 40 * i
mov 0x8(%rax), %rdx
# ap->idx = %rax + 8
# 这两句表明了&bp->a[i]的地址计算公式,即&bp+8+40i,因此可以说明,a的总大小是40
# +8说明first自己占8个字节,按照的8位对齐,因此a的第一个元素肯定是8个字节的.
movslq %ecx, %rcx
# 在这里将n进行了类型转换,int型转换成了long型,因此说明ap里的x数组一定是long型
mov %rcx, 0x10(%rax, %rdx, 8)
# 这句说明了ap->x[ap->idx]的地址计算公式是&bp + 16 + idx * 8
# +16说明了包含了first以及idx,说明idx是a的第一个元素,根据上面得出的第一个元素肯定是8个字节的结论,说明idx是long类型.
# 再因为一共占大小40,所以x数组的元素个数为(40 - 8) / 8 = 4
retq
- A.CNT = (288 - 8) / 40 = 7
1 - B.typedef struct {
long idx;
long x[4];
}
13
- A.e1.p:0,e1.y:8,e2.x :0,e2.next:8
- B.16
- C 代码分析
proc:
movq 8(%rdi), %rax
movq (%rax), %rdx
movq (%rdx), %rdx
subq 8(%rax), %rdx
movq %rdx, (%rdi)
ret
- 以得出C代码为:void proc(union ele *up){
up->e2.x = * ( *(up->e2.next).e1.p) - *(up->e2.next).e1.y;
}
14
#include <stdio.h>
void good_echo()
{
char str[SIZE];
while(1){
char *p = fgets(str, SIZE, stdin);
if (p == NULL) {
break;
}
printf("%s",p);
}
}
16
- 代码分析
aframe:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
# 将栈顶地址减小16
leaq 30(,%rdi,8), %rax
# %rax = 8 * n + 30
andq $-16, %rax
# 这里的原因跟课本中的那一处一样的道理,将后4位置0,成为最大的16的倍数.
subq %rax, %rsp
# 将栈顶地址减小%rax位
leaq 15(%rsp), %r8
andq $-16, %r8
# 这两句是保证了p的地址是16的倍数,取最小的16的倍数.
-
A.s2 = s1 - ((8 * n + 30) & 0xfffffff0).
因此:
if n % 2 == 0:
s2 = s1 - (8 * n + 16)
else:
s2 = s1 - (8 * n + 24) -
B:p = (s2 + 15) & 0xfffffff0
-
C.首先来看使e1最小,那么e2则是最大,如果要e2最大的话,因为这里是要16倍数的最小值,因此p最小则为某个对16取余为1的值,这时e2是15,e2不可能会大于等于16了.然后使e1+e2的和也最小,则是n为偶数时,是8n+16-8n,为16,因此答案是: e1为16-e2=1,此时n为偶数,s1%16=1.
使e1最大,则e2最小,e2最小则为p恰好是16的倍数,此时e2为0.然后使e1+e2的和也最大,则是n为奇数时,是8n+24-8n,为24,因此答案是: e1为24-e2=24,此时n为奇数,s1%16=0. -
D.s2保证了能容下8 * n字节的最小的16的倍数.
p保证了自身对16对齐.
15
- 用4个跳转
find_range:
vxorps %xmm1, %xmm1, %xmm1
vucomiss %xmm1, %xmm0
jp .L1
ja .L2
jb .L3
je .L4
.L2:
movl $2, %eax
jmp .Done
.L3:
movl $0, %eax
jmp .Done
.L4:
movl $1, %eax
jmp .Done
.L1:
movl $3, %eax
.Done
17
- 用条件传送
find_range:
vxorps %xmm1, %xmm1, %xmm1
movq $0, %r8
movq $1, %r9
movq $2, %r10
movq $3, %rax
vucomiss %xmm1, %xmm0
cmovl %r8, %rax
cmove %r9, %rax
cmova %r10, %rax
18
- A.对于第n个参数,则imag部分传%xmm(2n-1),real部分传%xmm(2n-2).
- B.imag部分返回值%xmm1, real部分返回值%xmm0.
给你的结对学习搭档讲解你的总结并获取反馈
- 由于娄老师在课上的推荐,我跟我的结对伙伴选取的都是第三章学习。在学期过程中我们一起讨论了第三章每节内容。
- 20155230
- 结对照片
- 结对学习内容
- 共同解决了学习中遇到的问题,并在博客第一板块体现。
- 共同完成了课后作业题。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 20/20 | 1/1 | 20/20小时 | |
第二周 | 50/70 | 1/2 | 5/25小时 | |
第三周 | 100/170 | 1/3 | 10/35小时 | |
第四周 | 68/238 | 1/4 | 20/55小时 | |
第五周 | 40/278 | 1/5 | 10/65小时 | |
第六周 | 100/378 | 1/6 | 10/75小时 | |
第七周 | 30/678 | 1/7 | 10/85小时 | |
第八周 | 200/878 | 1/8 | 10/95小时 | |
第九周 | 300/1178 | 1/9 | 10/105小时 | |
第十周 | 278/1456 | 1/10 | 10/115小时 | |
第十一周 | 307/1763 | 1/11 | 10/125小时 | |
第十二周 | 457/2220 | 1/12 | 10/135小时 | |
第十三周 | 612/2832 | 1/14 | 10/145小时 |
-
计划学习时间:20小时
-
实际学习时间:10小时
-
改进情况:
(有空多看看现代软件工程 课件
软件工程师能力自我评价表)