信息安全系统设计基础第四周学习总结

第三章

3.1历史观点

X86 寻址方式经历三代:

1 DOS时代的平坦模式,不区分用户空间和内核空间,很不安全 2 8086的分段模式 3 IA32的带保护模式的平坦模式

对于机器级编程来说,其中两种抽象尤为重要

1 机器级程序的格式和行为,定义为指令集体系结构(ISA),它定义了处理器状态,指令的格式,以及每条指令对状态的影响
2 机器级程序使用的存储器地址是虚拟地址,提供的存储器模型看上去是一个非常大的字节数组

 数据格式:由于是从16位体系结构扩展成32位,intel用术语字(word)表示16位数据类型,因此32位为双字(double words),64位数为4字(quad words)

 3.2程序编码

cc -01 -o p p1.c

表示使用第一级优化。优化的级别与编译时间和最终产生代码的形式都有关系,一般认为第二级优化-02 是较好的选择。

-o 表示将p1.c编译后的可执行文件命名为p

计算机系统使用了多种不同形式的抽象,对于机器级编程来说,两种抽象尤为重要。第一种是机器级程序的格式和行为,定义为指令集体系结构(ISA),他定义了处理器状态、指令的格式,以及每条指令对状态的影响。

1程序计数器(CS:IP)

2整数寄存器(AX,BX,CX,DX

3条件码寄存器(OF,SF,ZF,AF,PF,CF

4浮点寄存器

int accum = 0;int sum(int x, int y){ int t = x + y; accum += t; return t;}

在命令行上使用“-S”选项,就能得到C语言编译器产生的汇编代码:

unix> gcc -01 -S code.c

这会使GCC运行编译器产生一个汇编文件code.s,但不做其他进一步工作

 

如果在命令行上使用“-C”选项,GCC会编译并汇编该代码:

unix> gcc -01 -c code.c

这就会产生目标代码文件code.o,他是二进制格式,无法直接查看。

 

机器代码和它的反汇编表示的一些特性需要注意:

IA32指令长度从115个字节不等

设计指令格式的方式是,从某个给定位置开始,可以将字节唯一的解码成机器指令。

反汇编器只是基于机器代码文件中的字节序列来确定汇编代码,不需要访问程序的源代码或汇编代码。

反汇编器使用的指令命名规则与GCC生成的汇编代码使用的有些差别

3.3数据格式

Intel用术语word表示16位数据类型,32为double words,64位为quad words,这是由于最初Intel系列是从16位开始的。

ATT风格的汇编代码指令都有一个字符后缀,表面操作数的大小。Intel风格的汇编代码是没有的。

1.Intel位:字节16位:字32位:双字64位:四字

2.c语言基本数据类型对应的IA32表示

char 字节 1字节short 字 2字节int 双字 4字节long int 双字 4字节long long int (不支持) 4字节char * 双字 4字节float 单精度 4字节double 双精度 8字节long double 扩展精度 10/12字节

3.数据传送指令的三个变种:

movb 传送字节

movw 传送字

movl 传送双字

 

3.4 访问信息

IA32cpu中有832位的寄存器,%e是前缀,依次是:axcxdxbxsidispbp。前6个可以看作是通用寄存器,大多数情况下。前3个和后3个的保存和恢复惯例不同。最后两个指向程序栈中重要位置的指针。

指令的源数据值可以以常数形式给出,或是从寄存器或存储器中读出。也就是,常数,寄存器,内存。

IA32的一条限制:数据传送指令的两个操作数不能都指向存储器位置。

数据传送指令的源操作数在左,目的操作数在右(ATT风格),(Intel风格则相反)。

栈在程序的虚拟地址空间的上部,再往上就是内核虚拟空间了,栈底紧挨着内核虚拟空间,栈顶向下增长。%esp保存这栈顶元素的地址。

将一个双字压入栈,先将%esp减小4,然后将双字放入这多出来的4个字节的空间中。出栈则是先读出4个字节,然后%esp4

因为栈和程序代码以及其他形式的程序数据都是放在同样的存储器中(虚拟地址空间),所以程序用标准的存储器寻址方法访问栈内任意位置。

栈顶元素的地址是所有栈中元素地址中最低的

1.指针其实是地址,间接引用指针就是将该指针放在一个寄存器中 ,然后在间接存储器引用中引用这个寄存器

2.局部变量通常保存在寄存器中,而不是存储器(个人猜测应该是局部变量属于动态分配,局部变量因此被动态置入寄存器,而非存储器)

例如调用exchange:

 int a = 4;

 int b = exchange(&a,3);

printf("a=%d,b=%d\n",a,b);

打印出a=3,b=4

3.5 算术和逻辑操作

 加载有效地址地址指令,leal,只是将源操作数计算出来的地址交给目的操作数,貌似:源操作数是存储器访问格式的,目的操作数是寄存器。

 一元操作:incb/w/ldecb/w/lnegb/w/lnotb/w/l,前面两个好理解,第三个是取负,这里用到了一个概念,取负是指2变成-2-3变成3,可以认为,就将后面的操作数认为是补码编码的了,然后如果本来是2的补码,现在就要编程-2的补码,也就是进行0-2的运算。第四个是取反。

 二元操作:addb/w/lsubb/w/limulb/w/lxorb/w/lorb/w/landb/w/l。值得注意的是:第3个,乘是先数学上的乘,然后截断一下。这是2章中得到的结论,至少是对于imulb/w/l3者。后3个好理解,异或,或,并。

移位:salb/w/lshlb/w/lsarb/w/lshrb/w/l。向左left,向右right移位。向左总是补0的,向右就是算术移位和逻辑移位了。移位量是单个字节的编码,因为只允许031位的移位(只考虑移位量的低五位)。移位量只可以是一个立即数或者单字节寄存器元素%cl

 

3.7 过程

一个过程调用包括将数据和控制从代码的一部分传递到另一部分。过程调用就是函数。数据就是函数参数和返回值。控制应该就是当前运行的代码吧。另外,过程的局部变量的空间的分配和释放。LA32中,控制的转移是有相应的指令的,但是,数据传递和局部变量的分配释放通过操纵程序栈来实现。从这里看,过程调用分成3个部分:控制转移,数据传递,分配与释放局部变量。为单个过程分配的那部分栈称为栈帧。栈帧也可以看做有两种,一种是处于最低端的栈帧,也就是当前过程的栈帧;另一种就是处于栈中部的(非最低端)栈帧,也就是之前调用的过程,还没有返回。

每个栈帧的顶端都是%ebp,这里有两个意思,首先当前过程,拥有当前寄存器的值,%ebp寄存器的值是一个地址,地址是本栈帧最高字的地址。这个最高字记录着上一栈帧的最高字地址,以此类推。控制:其实表示的就是当前cpu在处理哪一个过程的代码,在处理那一个过程的代码,我们就说当前的控制在那一个过程。当前栈帧总是以%ebp中的值表示栈帧的最高字地址。最低字地址由%esp表示。中间栈帧最高字地址,保持在其下一个栈帧(也就是较低栈帧)的最高字节中,中间栈帧的最低字节都是返回地址,就是说,当较低栈帧代表的过程返回时,将跳转的代码的位置。栈的重点还是两个寄存器:%ebp和%esp。前者记录当前栈帧的最高字地址,后者记录栈的最低字地址,但其实也就是当前栈帧的最低字地址。一个是帧指针,一个是栈指针。栈帧中,可以保存:寄存器,本地变量,临时变量。

局部变量应该包括本地变量和临时变量,一般来说,局部变量保存在寄存器中,但也用栈来保存,用栈来保存的时候,一般有如下原因:没有足够多的寄存器存放所有变量(好理解)有些局部变量是数组和结构,因此必须通过数组和结构引用来访问。(就是通过地址来访问)要对一个局部变量使用地址操作符&,必须能够为它生成一个地址(好理解)当前栈帧的过程使用的参数都存放在上一栈帧中,上一栈帧的过程要使用的参数存放在上上一个栈帧中。

call指令的效果是:将返回地址入栈,并跳转到被调用过程的起始处。(这里的效果对栈来说,只有一点,就是入栈了返回地址,这句话之后,%esp就指向返回地址所在的位置了。这意味着旧栈帧已经封存了,但新的栈帧还没有出现,后半句跳转到被调用过程的起始处,说明了一点,过程的起始处有建立新栈帧的指令,新栈帧的建立是新过程自己完成的)

返回地址,就是call后面的指令的地址。

当调用过程返回时,执行会从此处继续。

ret指令的效果是:从栈中弹出地址,并跳转到这个位置。(这里有个重点,就是ret并不知道当前栈顶存储的是什么,他只是简单的弹出这个值,然后跳转去。所以要正确使用这个指令,必须,先要使得栈指针指向返回地址,这也就是说,新栈已经没有了,栈顶变成了旧栈的最后一个字。这个时候才可以使用ret指令,否则就不对了。)

leave指令的效果是:可以使栈做好返回的准备。(这里包含两点:1使得%ebp指向旧帧的最高字地址,2使得%esp指向旧帧返回地址。;使用这个指令之后,再使用ret,就没有问题。但这个指令不是必须的,其可以通过其他指令代替)

过程要返回整数或者指针的时候,寄存器%eax可以用来返回值。其实汇编级别的返回,就是在栈变化的时候(过程控制变化的时候)保持某一个寄存器不变,这样,返回的过程就可以使用这个寄存器,也就是说返回值了。相信,如果是double类型的,那么可能用两个寄存器返回也不一定,肯定是类似的过程。

调用者保存寄存器——%eax,%edx,%ecx。被调用者随便用。

被调用者保存寄存器——%ebx,%esi,%edi。被调用者要用这些的话,就需要先保存这些到栈,然后在ret前恢复这些寄存器。

此外,%ebp和%esp是要保持的,%esp还好,但%ebp是很需要维持的,算是被调用者保存寄存器了。因为这两个只用于当前过程。

gcc坚持一个x86编程指导方针:一个函数使用的所有栈空间,必须是16字节的整数倍。

这里突然想到了过程和函数是不同的,一个过程是顺序的,一个函数不一定,一个函数可以是顺序的,也可以不是,比如递归函数。对于递归函数,每一次的递归,对于机器语言来说,就是一个过程。所以过程和函数是不对应的。一个函数至少是一个过程。一个函数所使用的栈空间,必须是16字节的整数倍。如果是单一过程,也就是一个栈帧的情况,那这个栈帧就应该是16字节的倍数,如果多个过程,那么单个栈帧搞不好就可以没有16字节了,这个不多想了。一个过程的开始,通常是在call指令执行后,这个时候,旧帧已经封存了,但新帧还没有建立,所以个过程的开始要先建立新栈帧。通常是两个语句:pushl %ebp和movl %esp, %ebp。

也可能多一个pushl %ebx。

编译器根据一组很简单的惯例来产生管理栈结构的代码:

参数在栈上传递给函数,可以从栈中用相对于 %ebp的正偏移量来访问他们。

可以使用push或者从栈指针减去偏移量来在栈上分配空间。

返回前,函数必须将栈恢复到原始条件,可以恢复所有的被调用者保存寄存器和%ebp,并且重置%esp使其指向返回地址。

 

 

 

 

 

 

 

posted @ 2015-10-11 20:28  爱吃鸡翅膀  阅读(168)  评论(1编辑  收藏  举报