2017-2018-1 20155239 《信息安全系统设计基础》第13周学习总结
2017-2018-1 20155239 《信息安全系统设计基础》第13周学习总结
教材学习内容总结
目录
-
程序编码
-
栈帧结构
-
过程的实现
-
过程调用和返回指令
-
寄存器使用惯例
-
过程实例
-
递归过程
我个人认为虽然实践很重要,但是书本上的一些重点也是需要记录的,这样在以后的复习中就可以知道在当时学习的时候重点是什么,所以我还是要对第三章的重点做记录。
我的实践内容在总结的后面。
历史观点
- Intel处理器第一代是单芯片,16位微处理器
- 第一代是x8086,也就是上学期学的汇编语言
- 每个后继处理器的设计都是后向兼容的,可以保证较早版本上编译的代码在较新的处理器上运行。
程序编码
GCC将源代码转化为可执行代码的步骤:
- C预处理器——扩展源代码-生成.i文件
- 编译器——产生两个源代码的汇编代码-——生成.s文件
- 汇编器——将汇编代码转化成二进制目标代码——生成.o文件
- 链接器——产生可执行代码文件
命令gcc---GCC C、C++编译器。是Linux上默认的编译器。
gcc命令调用一系列程序,将源代码转化成可执行代码:
- C预处理器扩展源代码*.c,插入所有用#include命令指定的文件,并扩展所有用#define声明指定的宏。
- 编译器产生两个源代码的汇编代码,名字为*.s
- 汇编器将汇编代码转化成二进制目标代码(机器代码的一种形式)文件名为*.o
- 衔接器将两个目标代码文件与实现库函数的代码合并,并产生最终的可执行代码文件
2.1、机器级代码
1.机器级编程的两种抽象
(1)指令集结构ISA
是机器级程序的格式和行为,定义了处理器状态、指令的格式,以及每条指令对状态的影响。
(2)机器级程序使用的存储器地址是虚拟地址
看上去是一个非常大的字节数组,实际上是将多个硬件存储器和操作系统软件组合起来。
2.汇编代码的特点:
用可读性更好的文本格式来表示。
3.几个处理器:
- 程序计数器(CS:IP)
- 整数寄存器(AX,BX,CX,DX)
- 条件码寄存器(OF,SF,ZF,AF,PF,CF)
- 浮点寄存器
一条机器指令只执行一个非常基本的操作。
程序编码
书上107页的代码,需要用到反汇编器。在Linux系统中,带‘d’命令行标志的程序OBJDUMP可以充当这个角色。
objdump -d xxx.xx
数据格式
1.Intel中:
8 位:字节
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 传送双字
访问信息
一个IA32中央处理单元(CPU)包含8个存储32位置的寄存器
操作数指示符
- 立即数
- 寄存器
- 存储器
寻址方式
(1)立即数寻址方式
格式:$后加用标准c表示法表示的整数,如$0xAFF
(2)寄存器寻址方式
如%eax,与汇编中学过的AX寄存器类比。
(3)存储器寻址方式
- 直接寻址方式
- 寄存器间接寻址方式
- 寄存器相对寻址方式
- 基址变址寻址方式
- 相对基址变址寻址方式
数据传送指令
MOV
- movb 传送字节
- movw 传送字
- movl 传送双字
MOVS
- movsbw 将做了符号扩展的字节传送到字
- movsbl 将做了符号扩展的字节传送到双字
- movswl 将做了符号扩展的字传送到双字
MOVZ
- movzbw 将做了零扩展的字节传送到字
- movzbl 将做了零扩展的字节传送到双字
- movzwl 将做了零扩展的字传送到双字
算术和逻辑操作
一、加载有效地址
加载有效地址指令——leal,是movl指令的变形。
指令形式:从存储器读取数据到寄存器。
实际:将有效地址写入到目的操作数,而目的操作数必须是寄存器;并不真实引用存储器。
机器代码:二进制格式
汇编代码:用可读性更好的文本格式来表示。
一条机器指令值执行一个非常基本的操作。
2.2、代码示例
-
gcc -o1 –S code.c
gcc将.c文件编译器产生可读汇编代码文件
-
gcc –o1 –C code.c
gcc将.c文件编译并汇编该代码,二进制格式。
-
objdump –d code.o
用于查看目标代码文件的内容,将目标文件反汇编为汇编代码文件.s
-
gcc –o1 –o main.c prog
生成可执行文件
1.栈帧结构
如果要学明白栈帧结构,首先要知道栈帧是什么?从书中,我看到对栈帧的概念是:
为单个过程分配的那部分栈就叫做栈帧。栈帧的最顶端以两个指针界定——帧指针(寄存器ebp)和栈指针(esp)。
不过我认为对刚接触的我们来说,就是将其看做一个函数即可。既栈帧就是帧指针ebp和栈指针esp之间的内容,即函数主体,ebp指向函数头,位置确定,esp指向函数尾,随函数内部变量的增加或减少而移动。
每一个被调用的函数都有一个自己的栈帧结构,并且栈帧结构是由函数自己形成的。需要注意的是:CPU中的寄存器ebp和esp都只有一个。
我的疑问是:每一个被调用的函数都有一个自己的栈帧结构,但是一段代码都有一个函数,没有界限,但是栈帧的寄存器ebp和esp都只有一个,所以如何来协调使用完成函数调用的呢?
首先学习了地址空间分配的问题,如下图:
我上网查了一些资料并进行了学习和采纳,如IA32程序用程序栈来支持过程调用。机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。而为单个过程分配的那部分栈称为帧栈(stack frame)。
帧栈可以认为是程序栈的一段,它有两个端点,一个标识着起始地址,一个标识着结束地址,而这两个地址,则分别存储在固定的寄存器当中,即起始地址存在%ebp寄存器当中,结束地址存在%esp寄存器当中。也就是说寄存器 %ebp 为帧指针,寄存器 %esp 为栈指针。
当程序执行时,栈指针可以移动,因此大多数信息的访问都是相对于帧指针的。
如图:
上图包含了程序栈的构成,它由一系列栈帧构成,这些栈帧每一个都对应一个过程,而且每一个帧指针+4的位置都存储着函数的返回地址,每一个帧指针指向的存储器位置当中都备份着调用者的帧指针。各位需要知道的是,每一个栈帧都建立在调用者的下方(也就是地址递减的方向),当被调用者执行完毕时,这一段栈帧会被释放。还有一点很重要的是,%ebp和%esp的值指示着栈帧的两端,而栈指针会在运行时移动,所以大部分时候,在访问存储器的时候会基于帧指针访问,因为在一直移动的栈指针无法根据偏移量准确的定位一个存储器位置。
对这一块内容学习的收获:
从书中的学习和查资料对这一小块的内容有了更多的了解,比如栈的空间分配和释放。栈帧是向地址递减的方向延伸,所以要想给栈分配空间,可以将栈的指针减去一个空间的值,形象的来说,就是栈的指针上移和下移就是对栈空间的分配和释放,也就是栈变长或者变短。
过程的实现
首先要明白什么是过程的实现?我的理解就是数据在调用者和被调用者之间传递,以及在被调用者当中局部变量内存的分配以及释放。
那么这一过程是如何实现的?
我上网学习了一些资料和博客,参数传递以及局部变量内存的分配和释放有下面的几个操作,这些操作都是我们经常产生汇编代码所看到的东西。
1.以下汇编代码的实现就是备份原来的帧指针,调整当前的帧指针到栈指针的位置。
pushl %ebp
movl %esp, %ebp
2.以下代码的实现就是创建的栈帧给被调用者准备,当被调用者使用栈帧时,需要给临时变量分配预留内存
subl $16,%esp
3.以下代码处理备份被调用者保存的寄存器当中的值果有值的话,备份的方式则压入栈顶
pushl %ebx
4.使用建立好的栈帧,比如读取和写入,一般使用mov,push以及pop指令等等。
5.恢复被调用者寄存器当中的值,这一过程其实是从栈帧中将备份的值再恢复到寄存器,不过此时这些值可能已经不在栈顶了。因此在恢复时,大多数会使用pop指令,但也并非一定如此。
6.释放被调用者的栈帧,释放就意味着将栈指针加大,而具体的做法一般是直接将栈指针指向帧指针,因此会采用类似下面的汇编代码处理(也可能是addl)。
movl %ebp,%esp
7.恢复调用者的栈帧,恢复其实就是调整栈帧两端,使得当前栈帧的区域又回到了原始的位置。因为栈指针已经在第六步调整好了,因此此时只需要将备份的原帧指针弹出到%ebp即可。类似的汇编代码如下。
popl %ebp
8.弹出返回地址,跳出当前过程,继续执行调用者的代码。此时会将栈顶的返回地址弹出到PC,然后程序将按照弹出的返回地址继续执行。这个过程一般使用ret指令完成。
过程调用和返回指令
学习了一些过程调用和返回的指令:
指令:
- call指令
- leavel指令
- ret指令
首先来学习什么是call指令:
hello.c -> 预处理 hello.i -> 编译阶段 hello.s -> 汇编阶段 hello.o -> 链接阶段 ->hello
从上面过程来看,经过编译阶段生成汇编是必须的。
首先先写一个hello.c程序
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
汇编产生的代码如下;
在hello.c中只调用了一个库函数就是printf所以代码里有callq和retq看看下面几行
400520: e8 bb fe ff ff callq 4003e0 <puts@plt>
400525: b8 00 00 00 00 mov $0x0,%eax
40052a: c9 leaveq
40052b: c3 retq
最前面是内存地址(虚拟地址),接着是地址内存的机器指令,接下来是汇编代码call指令调用printf函数时,先把下一条指令 mov %0x0, %eax 压栈,然后跳转到printf函数执行printf函数。执行完了执行ret指令,ret指令是从栈中弹出刚才压栈的指令,继续执行此指令和后面的指令。栈段也就是起了一个临时保存的作用。所以ret和call是配合着使用的。
call 和 ret
call: 将当前的IP 或者 CS:IP 压入栈中跳转到指定位置
ret : 用栈中所保存的数据赋值给IP的, 跳转回来。
寄存器使用惯例
程序寄存器组是唯一能够被所有过程共享的资源。虽然在给定时刻只能有一个过程是活动的,但是我们必须保证当一个过程(调用者)调用另一个过程(被调用者)时,被调用者不会覆盖某个调用者稍后会使用的寄存器的值。为此必须采用一组统一的寄存器使用惯例,所有的过程都必须遵守,包括程序库的过程。
假如没有这些规矩,比如在调用一个过程时,无论是调用者还是被调用者,都可能更新寄存器的值。假设调用者在%edx中存了一个整数值100,而被调用者也使用这个寄存器,并更新成了1000,于是悲剧就发生了。当过程调用完毕返回后,调用者再使用%edx的时候,值已经从100变成了1000,这几乎必将导致程序会错误的执行下去。所以便有如下规矩:
在 IA32 中,寄存器%eax,%edx和%ecx被划分为调用者保存寄存器。当过程 P 调用 Q 时,Q可以覆盖这些寄存器,而不会破坏 P 所需的数据。
寄存器%ebx,%esi和%edi被划分为被调用者保存寄存器。这里 Q 必须在覆盖这些寄存器的值之前,先把他们保存到栈中,并在返回前恢复它们,因为 P(或某个更高层次的过程)可能会在今后的计算中需要这些值。上面所说的过程实现的8个步骤中第三步便是如此。
考虑如下代码:
int P(int x)
{
int y = x*x;
int z = Q(y);
return y+z;
}
过程 P 在调用 Q之前会先计算 y 的值,而且它必须保证 y 的值在 Q返回后是可用的。这里有两种方法实现:
-
可以在调用 Q 之前,将 y 的值保存在自己的帧栈中;当 Q 返回时,过程 P 就可以从栈中取出y 的值。换句话说就是调用者 P 自己保存这个值。
-
可以将 y 保存在被调用者保存寄存器中。如果 Q ,或者其它 Q 调用的程序想使用这个寄存器,它必须将这个寄存器的值保存在帧栈中,并在返回前恢复该值。换句话说就是被调用者保存这个值。当 Q 返回到 P 时,y 的值会在被调用者保存寄存器中,或者是因为寄存器根本就没有改变,或者是因为它被保存并恢复了。
过程实例
编写一个代码:function.c
#include <stdio.h>
int add(int a,int b){
register int c = a + b;
return c;
}
int main(){
int a = 100;
int b = 101;
int c = add(a,b);
return c;
}
在主函数过程中调用add过程。
gcc -O0 -S function.c
为了完整的展现那8个步骤,因此给变量c加了register关键字修饰,这将会将c送入寄存器,从而更改被调用者保存寄存器,就会导致步骤3的发生。以下是main函数以及add函数各自的栈帧情况:
对以上学习内容的总结:
1.add函数会将返回结果存入%eax(前提是返回值可以使用整数来表示),在main函数中,call指令之后,默认将%eax作为返回结果来使用。
2.所有函数(包括main函数)都必须有第1步和第6、7、8步,这是必须的4步。我们的栈指针和帧指针有固定的大小关系,即栈指针永远小于等于帧指针,当二者相等时,当前栈帧被认为没有分配内存空间。
递归过程
一个过程也能调用自己本身的,就是递归调用。因为每个调用在栈中都有它自己的私人空间,多个未完成调用的局部变量不会互相影响栈的原则也提供了适当的策略,当过程被调用时分布局部存储空间,当过程执行完毕返回时释放存储空间。
下面是一段求 n 的阶乘的递归调用代码:
int rfact(int n){
int result;
if(n<=1){
result = 1;
}else{
result = n * rfact(n-1);
}
return result;
}
产生汇编代码:
上面的汇编代码,当用参数 n 来调用时,首先代码 2~5 行会创建一个帧栈,其中包含 %ebp 的旧值、保存的被调用者保存的寄存器 %ebx 的值,以及当递归调用自身的时候保存参数的四个字节。
如下图所示,它用寄存器 %ebx 来保存过程参数 n 的值(第 6 行代码)。它将寄存器 %ebx 中的返回值设置为 1,预期 n<=1 的情况,它就会跳转到完成代码。
对于递归的情况,计算 n-1,将这个值存储在栈上,然后调用函数自身(第10~12行),在代码的完成部分,我们可以假设:
①、寄存器%eax保存这(n-1)!的值
②、被调用保存寄存器%ebx保存着参数n
因此将这两个值相乘(第 13 行)得到该函数的返回值。对于终止条件和递归调用,代码都会继续到完成部分(第15~17行),恢复栈和被调用者保存寄存器,然后在返回。
所以我们看到递归调用一个函数本身与调用其它函数是一样的。栈规则提供了一种机制,每次函数调用都有它自己的私有状态信息(保存的返回值、栈指针和被调用者保存寄存器的值)存储。如果需要,它还可以提供局部变量的存储。分配和释放的栈规则很自然的就与函数调用——返回的顺序匹配
二、一元操作和二元操作
1.一元操作
只有一个操作数,既是源又是目的,可以是一个寄存器,或者存储器位置。
2.二元操作
第一个操作数可以是立即数、寄存器或者存储器位置
第二个操作数可以是寄存器或者存储器位置
但是不能同时是存储器位置。
三、移位操作
SAL 算术左移
SHL 逻辑左移
SAR 算术右移(补符号位)
SHR 逻辑右移(补0)
控制
一、条件码
CF:进位标志
ZF:零标志
SF:符号标志
OF:溢出标志
条件码的改变:
数据传送指令
MOV 不影响标志位
PUSH POP 不影响标志位
XCHG 交换指令 不影响标志位
XLAT 换码指令 不影响标志位
LEA 有效地址送寄存器指令 不影响标志位
PUSHF 标志进栈指令 不影响标志位
POPF 标志出栈指令 标志位由装入值决定
二、访问条件码
这个指的是SET指令,通过set与不同的条件码的组合,达到不同的跳转条件。
某些底层的机器指令可能有多个名字,我们称之为“同义名”。
三、跳转指令及其编码
jump分为直接跳转和间接跳转:
直接跳转:后面跟标号作为跳转目标
间接跳转:*后面跟一个操作数指示符
四、循环
C语言提供了多种循环结构。即do-while,while,for。汇编中没有相应的指令存在,可以用条件测试和跳转组合起来实现循环的效果。
循环的实践我在下面写出。
过程
一个过程调用包括将数据和控制从代码的一部分传递到另一部分。另外,它还必须在进入时为过程的局部变量分配空间,并在退出时释放这些空间。
一、栈帧结构
栈用来传递参数、存储返回信息、保存寄存器,以及本地存储。
1.栈帧
为单个过程分配的那部分栈称为栈帧,通用结构见149页
所以本质上栈帧还是栈。
2.两个指针
最顶端的栈帧以两个指针界定:
寄存器%ebp-帧指针
寄存器%esp-栈指针
栈指针可移动,所以信息访问多相对于帧指针。
教材学习中的问题和解决过程
在开始实践的时候,我在编译时候按照书上的指令来执行但是缺没办法生成正确的.o文件
都赖我想起来,老师上课说过,因为版本不一样,所以在敲指令的时候,要把-o去掉,这样就可以生成可以检验的汇编语言代码。
最后,我还掌握了另外一种看汇编代码的指令:
cat xxx.s
还有一种就是书上的代码方式:
objdump -d xxx.o
实验楼中的练习:
我的代码中汇编是movq %rbq,因为是我没使用-m32。
在64位下所以是movq %rbq。
书上还有几个实践,其中看代码的二进制代码: