2017-2018-1 学号20155209 《信息安全系统设计基础》第十三周学习总结

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2017-2018-1 学号20155209 《信息安全系统设计基础》第十三周学习总结相关的知识,希望对你有一定的参考价值。

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->SF^OF(有符号的小于):l代表的是less。这里的组合是SF^OF,即对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,相对来说就比较复杂了。不过有了前面的铺垫,这个也非常好理解。SF^OF代表小于,则~(SF^OF)代表大于等于,而~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\\n",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\\n",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 标准中,浮点数是将特定长度的连续字节的所有二进制位分割为特定宽度的符号域,指数域和尾数域三个域,其中保存的值分别用于表示给定二进制浮点数中的符号,指数和尾数。这样,通过尾数和可以调节的指数(所以称为"浮点")就可以表达给定的数值了。

    我的问题及理解

  • 本章问题都在习题中理解了。

    课后作业

    3.58

long decode2(long x, long y, long z)
{
int result = x * (y - z);
if((y - z) & 1)
result = ~result;
return result;
}

3.59

这个题考察的是2.3.4和2.3.5节的一个定理:w比特长度的两个数相乘,会产生一个2w长度的数,不管这两个数是无符号数还是补码表示的有符号数,把结果截取的低w比特都是相同的。

所以我们可以用无符号数乘法指令mulq实现有符号数乘法:先把数有符号扩展致2w位,然后把这两个2w位的数相乘,截取低2w位即可。

截取就是求模运算,即 mod 2^w。

store_prod
movq %rdx, %rax #rax中保存y
cqto #将rax有符号扩展为rdx:rax,即rdx为全1
movq %rsi, %rcx #rcx中保存x
sarq $63, %rcx #rcx为为全1若x小于0,否则为0,即将x有符号扩展
#下面把这两个扩展的数当成无符号数进行运算,取低128bit。
#此时y表示为rdx:rax,x表示为rcx:rsi, 即y = rdx2^64 + rax, x = rcx2^64 + rsi
#xy = rdxrcx2^128 + rdxrsi2^64 + rcxrax2^64 + raxrsi
#由于我们只需要取低128位,所以对xy进行取模操作mod 128,得到公式:rdxrsi2^64mod2^128 + rcxrax2^64mod2^128 + raxrsi
#由于这里的寄存器都是64位的,所以对于rdxrsi2^64mod2^128这样的操作我们可以直接使用imulq指令,截取两个寄存器相乘的低64位,然后把他加到raxrsi的高64位。
#下面实现公式
imulq %rax, %rcx #rcx
rax2^64mod2^128(随后放在高64位)
imulq %rsi, %rdx #rdx
rsi2^64mod2^128(随后放在高64位)
addq %rdx, %rcx #随后放在高64位
mulq %rsi #x
y即rax*rsi
addq %rcx, %rdx #放在高64位
movq %rax, (%rdi) #存储低64位
movq %rdx, 8(%rdi) #存储高64位
ret

3.60

A. x : %rdi n : %esi result : %rax mask : %rdx

B. result = 0 mask = 1

C. mask != 0

D. mask >>= n

E. result |= (x & mask)

long loop(long x, int n)
{
long result = 0;
long mask;
for(mask = 1; mask != 0; mask >>= n)
{
result |= (x & mask);
}
return result;
}

3.61

long cread_alt(long xp)
{
static long tmp = 0;
if(xp == 0)
{
xp = &tmp;
}
return
xp;
}

这个地方也是很无语,在我的环境下必须将tmp的存储类型设置为静态存储,并且将gcc的优化设置为O3,这样才能生成使用conditional transfer的指令(才能让gcc相信优化是值得的。。):

00000000004004f0

3.62

typedef enum {MODE_A, MODE_B, MODE_C, MODE_D, MODE_E}
long switch3(long p1, long p2, mode_t action)
{
long result = 0;
switch(action)
{
case MODE_A:
result = p2;
p2 = *p1;
break;

case MODE_B:
  result = *p1 + *p2;
  *p1 = result;
  break;
  
case MODE_C:
  *p1 = 59;
  result = *p2;
  break;
  
case MODE_D:
  *p1 = *p2;
  
case MODE_E:
  result = 27;
  break;
  
default:
  result = 12;

}
return result;
}

3.63

long switch_prob(long x, long n)
{
long result = x;
switch(n)
{
case 0:
case 2:
result += 8;
break;
case 3:
result >>= 3;
break;
case 4:
result = (result << 4) - x;
case 5:
result *= result;
default:
result += 0x4b;
}
return result;
}

3.64

A. &A[i][j][k] = Xa + L(iST + j*T + k)

B. R = 7,S = 5,T = 13

3.65

A. rdx (每次移位8,即按行移动)

B. rax(每次移位120 = 8 * 15,按列移动)

C. 由B,M = 15

3.66

NR(n)是数组的行数,所以我们找循环的次数,即rdi,得到rdi = 3n.

NC(n)是数组的列数,所以我们应该找每次循环更新时对指针增加的值,这个值等于sizeof(long) * NC(n),即r8,得到r8 = 8 * (4n + 1).

综上,可知两个宏定义:

define NR(n) (3*(n))

define NC(n) (4*(n)+1)

3.67

A.

B. %rsp + 64

C. 通过以%rsp作为基地址,偏移8、16、24来获取strA s的内容(由于中间夹了一个返回地址,所以都要加8)

D. 通过传进来的参数%rdi(%rsp + 64 + 8),以此作为基地址,偏移8、16、24来写入strB r

E.

F. 我记得我在看《C语言程序设计: 现代方法 2rd》的时候,里面说传递聚合类型的变量可以使用指针,这样比传递整个数据结构要快一些(当然写操作会改变实参)。这个题目里面也都是读操作,可以发现编译器自动进行了优化——传递了基地址而非复制了整个数据结构。返回就是在调用它的函数的栈帧中存入一个相关的数据结构。(这个题里面process其实没有栈帧,如果返回地址算eval的话)3.68

这题考察的是内存对齐。通过结构体成员的位置逐渐缩小范围:

int t 为8(%rsi),所以4<B<=8
long u 为32(%rsi),所以24 < 8 + 4 + 2*a <= 32,得到6<A<=10
long y 为184(%rdi),所以176 < 4*A*B <= 184,得44 < A*B <=46。

所以AB = 45 或者AB = 46,结合A, B各自的范围,只可能为A = 9, B = 5.

3.69

A. 根据第4、5行的指令, idx的值为(bp + 40i + 8),由第1、2行指令,这里的8是因为第一个int first整数和内存对齐的原因,所以每一个a_struct的大小为40字节。

由于0x120 - 0x8 = 280字节,所以CNT = 280/40 = 7.

B. 由第6、7行指令知,idx和x数组内元素都是signed long类型的。由于整个a_struct数据类型大小为40字节,所以其内部应该为85 = 8 + 84:

typedef struct
{
long idx;
long x[4];
}

3.70

A.

e1.p : 0

e1.y : 8

e2.x : 0

e2.next : 8

B. 16 bytes

C.

void proc(union ele up)
{
up->x =
(up->e2.next->e1.p) - up->e2.next->e1.y;
}

3.71

include

include

include

define SIZE_OF_BUFFER 10

int good_echo(void)
{
char buffer = calloc(SIZE_OF_BUFFER, sizeof(char));
if (buffer == NULL)
{
fprintf(stderr, "Error: failed to allocate buffer.\\n");
return -1;
}
while(1)
{
fgets(buffer, SIZE_OF_BUFFER, stdin);
if (strlen(buffer) == SIZE_OF_BUFFER-1) /
两种情况,一种是刚好输入了能填满缓冲区的字符数,另一种是大于缓冲区,一次不能读完/
{
fputs(buffer, stdout);
if (buffer[SIZE_OF_BUFFER-1-1] == ‘\\n‘)/
刚好输入了能填满缓冲区的字符数,结束读入/
{
break;
}
memset(buffer, 0, strlen(buffer));/
清空缓冲区,因为要通过strlen判断读入了多少字符,继续读入/
}
else if (strlen(buffer) < SIZE_OF_BUFFER-1)/
一定是最后一次读入,结束读入*/
{
fputs(buffer, stdout);
break;
}
else
{
break;
}
}
free(buffer);
return 0;
}

int main(int argc, char const *argv[])
{
return good_echo();
}

3.72

A. andq $-16, X这条指令相当于将低4位置零,也就是使得rax中保存的8n+30对16取整。所以s2-s1为8n+30对16取整的结果。

B. p的值为rsp(r8)-15对16取整的结果,确保了p数组的起始地址为16的整数倍。

C. 8n + 30对16取整有两种可能:一种是8n本身就是16的整数倍即n = 2k,此时取整后为8n+16; 另一种是8n = 16k + 8即n = 2k + 1,此时取整后为8n + 24。由System V AMD64 ABI标准可知,s1的地址为16的整数倍(即结尾为0000),所以s2的地址也肯定是16的整数倍(结尾为0000)。又因p是由s2减15对16取整得到的结果,所以p和s2之间肯定相差2字节,即e2 = 2 bytes. 所以e1最大为(n为奇数) :8n + 24 - 16 - 8n = 8 bit, 最小为(n为偶数):8n + 16 -16 - 8n = 0.(这个题我估计没有考虑到ABI标准对于栈帧对齐的问题,s1的地址本来就应该是16的整数倍)

D. 由A B C可知,这种方法保证了s2 和 p的起始地址为16的整数倍,而且保证了e1最小为8n,能够存储p数组。

浮点数部分并未测试
3.73

find_range:
vxorps %xmm1, %xmm1, %xmm1
vucomiss %xmm0, %xmm1
ja .L5
jp .L8
movl $1, %eax
je .L3
.L8:
seta %al
movzbl %al, %eax
addl $2, %eax
ret
.L5:
movl $0, %eax
.L3:
rep;ret

3.74

find_range:
vxorps %xmm1, %xmm1, %xmm1
vucomiss %xmm0, %xmm1
cmova $0, %eax
cmove $1, %eax
cmovb $2, %eax
cmovp $3, %eax
rep;ret

3.75

A. 每一个复数变量使用两个%xmm寄存器传送。

B. 通过%xmm0和%xmm1返回一个复数类型值。












































































































































以上是关于2017-2018-1 学号20155209 《信息安全系统设计基础》第十三周学习总结的主要内容,如果未能解决你的问题,请参考以下文章

2017-2018-1 学号20155209 《信息安全系统设计基础》第十一周学习总结

2017-2018-1 学号20155209 《信息安全系统设计基础》第十周学习总结

2017-2018-1 20155209 实验三 实时系统

2017-2018-1 20155209 实现mypwd

2017-2018-1 20155209 实验五 通讯协议设计

2017-2018-1 学号20155311 《信息安全系统设计基础》第9周学习总结