20155236 《信息安全系统设计基础》第14周学习总结
程序的机器级表示
历史观点
- Intel处理器系列:俗称x86,开始时是第一代单芯片、16位微处理器之一。
- DOS时代的平坦模式,不区分用户空间和内核空间,很不安全;
- 8086的分段模式;
- IA32的带保护模式的平坦模式
- 每个后继处理器的设计都是后向兼容的,可以保证较早版本上编译的代码在较新的处理器上运行。
程序编码
-
GCC将源代码转化为可执行代码的步骤:
- C预处理器——扩展源代码-生成.i文件
- 编译器——产生两个源代码的汇编代码-——生成.s文件
- 汇编器——将汇编代码转化成二进制目标代码——生成.o文件
- 链接器——产生可执行代码文件
-
两种抽象:
- 指令集结构ISA:是机器级程序的格式和行为,定义了处理器状态、指令的格式,以及每条指令对状态的影响。
- 机器级程序使用的存储器地址是虚拟地址,看上去是一个非常大的字节数组,实际上是将多个硬件存储器和操作系统软件组合起来。
-
C预处理器插入宏和头文件:
gcc -E xxx.c -o xxx.i
编译器产生源代码的汇编代码:gcc -S xxx.i -o xxx.s
汇编器化成二进制目标代码:gcc -c xxx.s -o xxx.o
链接器生成最终可执行文件:gcc xxx. -o xxx
用objdump -d xxx.o -o
反汇编
3.3数据格式
- 大多数GCC生成的汇编代码指令都有一个字符后缀,表明操作数的大小。
- C语言数据类型在X86-64中的大小。在64位机器中,指针长8字节。
C声明 | Intel数据类型 | 汇编代码后缀 | 大小(字节) |
---|---|---|---|
char | 字节 | b | 1 |
short | 字 | w | 2 |
int | 双字 | l | 4 |
long | 四字 | q | 8 |
char* | 四字 | q | 8 |
float | 单精度 | s | 4 |
double | 双精度 | l | 8 |
3.4访问信息
寄存器
- 一个IA32中央处理单元(CPU)包含一组8个存储32位值的寄存器。用来存储整数数据和指针。
%eax %ax (%ah %al) 通用寄存器
%ecx %cx (%ch %cl) 通用寄存器
%edx %dx (%dh %dl) 通用寄存器
%ebx %bx (%bh %bl) 通用寄存器
%esi %si 用来操纵数组
%edi %di 用来操纵数组
%esp %sp 操纵栈帧
%ebp %bp 操纵栈帧
-
对于整数寄存器
-
esi edi可以用来操纵数组
-
esp ebp用来操纵栈帧
-
32位的eax,16位的ax,8位的ah,al都是独立的
-
操作数格式(s=1, 2, 4, 8)
格式 | 操作数值 |
---|---|
Imm(rb, ri, s) | M[Imm+R[rb]+R[ri]·s] |
-
MOV相当于赋值
-
不能从内存直接MOV到另一个内存,要用寄存器中转
-
MOV指令示例(源操作数,目的操作数)
-
MOV指令大小匹配,MOVZ和MOVS将较小的源值复制到较大的目的
-
MOVZ类将目的中剩余的字节填充为0
-
MOVS类通过符号扩展来填充,把源操作数的最高位进行赋值
-
注意
- 对于32位的eax,16位的ax,8位的ah,al都是独立的,我们通过下面例子说明:
假定当前是32位x86机器,eax寄存器的值为0x8226,执行完addw $0x8266
,%ax指令后eax的值是多少?
解析:0x8226+0x826=0x1044c, ax是16位寄存器,出现溢出,最高位的1会
丢掉,剩下0x44c,不要以为eax是32位的不会发生溢出.
寻址方式
- 根据操作数的不同类型,寻址方式可分为以下三种:
- 立即数寻址方式:操作数为常数值,写作$后加一个整数。
- 寄存器寻址方式:操作数为某个寄存器中的内容。
- 存储器寻址方式:根据计算出来的地址访问某个存储器的位置。
- 寻址模式:一个立即数偏移Imm,一个基址寄存器Eb,一个变址寄存器Ei,一个比例因子s(必须为1,2,4,8)有效地址计算为:
Imm(Eb,Ei,s) = Imm + R[Eb] + R[Ei]*s
数据传送指令
- MOV相当于C语言的赋值'='
- mov S,D S中的字节传送到D中
注意
- ATT格式中的方向;
- 不能从内存地址直接MOV到另一个内存地址,要用寄存器中转一下;
- 区分MOV,MOVS(符号扩展),MOVZ(零扩展)
- push和pop:
pushl S R[%esp] ← R[%esp]-4
M[R[%esp]] ← S
popl D D ← M[R[%esp]]
R[%esp] ← R[%esp]+4
注意
- 栈顶元素的地址是所有栈中元素地址中最低的,后进先出;
- 指针就是地址;局部变量保存在寄存器中。
算术和逻辑操作
加载有效地址
- leal,从存储器读数据到寄存器,而从存储器引用的过程实际上是将有效地址写入到目的操作数。
- 目的操作数必须是一个寄存器。
一元操作和二元操作
- 一元操作:只有一个操作数,既是源又是目的,可以是一个寄存器或者存储器。
- 二元操作:第二个操作数既是源又是目的,两个操作数不能同时是存储器。
移位
- 先给出位移量,然后是位移的数值,可进行算数和逻辑右移。
- 移位操作移位量可以是立即数或%cl中的数。
控制
条件码
-
描述最近的算数或者逻辑操作的属性,可以检测这些寄存器来执行条件分支指令。
-
CF:进位标志,最近操作使高位产生进位,用来检测无符号操作数的溢出
-
ZF:零标志,最近操作得出的结果为0
-
SF:符号标志,最近操作得到的结果为负数
-
OF:溢出标志,最近操作导致一个补码溢出-正溢出或负溢出。
注意
- leal不改变条件码寄存器
- CMP与SUB的区别:CMP也是根据两个操作数之差设置条件码,但只设置条件码而不更新目标寄存器
- 有条件跳转的条件看状态寄存器(教材上叫条件码寄存器)
访问条件码的读取方式
- 根据条件码的某个组合,将一个字节设置成0或1;
- 跳转到程序某个其他的部分;
- 有条件的传送数据。
- SET指令根据t=a-b的结果设置条件码
跳转指令及其编码
- 控制中最核心的是跳转语句:
- 有条件跳转(实现if,switch,while,for)
- 无条件跳转jmp(实现goto)
- 当执行PC相关的寻址时,程序计数器的值是跳转指令后面那条指令的地址,而不是跳转指令本身的地址。
翻译条件分支
- 将条件和表达式从C语言翻译成机器代码,最常用的方式是结合有条件和无条件跳转。
- C语言中if-else语句的通用形式:
if(test-expr)
then-statement
else
else-statement
- 汇编结构:
t=test-expr;
if!(t)
goto false;
then-statement
goto done;
false:
else-statement
done:
循环
- do-while循环
- C语言中do-while语句的通用形式:
do
body-statement
while(test-expr);
- 汇编结构:
loop:
body-statement
t=test-expr;
if(t)
goto loop;
- while循环
- C语言中while语句的通用形式:
while(test-expr)
body-statement
- 汇编结构:
t=test-expr;
if(!t)
goto done;
loop:
body-statement
t=test-expr;
if(t)
goto loop;
done:
- for循环
- C语言中for语句的通用形式:
for(init-expr;test-expr;update-expr)
body-statement
- 汇编结构
init-expr
t=test-expr;
if(!t)
goto done;
loop:
body-statement
update-expr;
t=test-expr;
if(t)
goto loop;
done:
- switch语句
- 根据一个整数索引值进行多重分支,执行switch语句的关键步骤是通过跳转表来访问代码位置,使结构更加高效。
过程
-
数据传递、局部变量的分配和释放通过操纵程序栈来实现。
-
C语言过程调用机制使用了栈数据结构提供的后进先出的内存管理原则。
-
函数调用的栈帧结构用来传递参数、存储返回地址、保存寄存器及本地存储
-
P(调用者)调用Q(被调用者),则Q的参数放在P的栈帧中。调用Q是P的返回地址被压入栈中,形成P的栈帧的末尾。返回地址是程序从Q返回时应该继续执行的地方。Q的栈帧从保存的帧指针如%ebp开始,后面是保存的寄存器值。
栈帧结构
- 为单个过程分配的栈叫做栈帧,寄存器%ebp为帧指针,而寄存器指针%esp为栈指针,程序执行时栈指针移动,大多数信息的访问都是相对于帧指针。
- 栈向低地址方向增长,而栈指针%esp指向栈顶元素。
转移控制
- call:目标是指明被调用过程起始的指令地址,效果是将返回地址入栈,并跳转到被调用过程的起始处。
- ret:从栈中弹出地址,并跳转到这个位置。
- 函数返回值存在%eax中
- call和ret指令的一般形式:
指令 | 描述 |
---|---|
call Label | 过程调用 |
call *Operand | 过程调用 |
ret | 从过程调用中返回 |
寻址方式
- 根据操作数的不同类型,寻址方式可分为以下三种:
- 立即数寻址方式:操作数为常数值,写作$后加一个整数。
- 寄存器寻址方式:操作数为某个寄存器中的内容。
- 存储器寻址方式:根据计算出来的地址访问某个存储器的位置。
- 寻址模式:一个立即数偏移Imm,一个基址寄存器Eb,一个变址寄存器Ei,一个比例因子s(必须为1,2,4,8)有效地址计算为:
Imm(Eb,Ei,s) = Imm + R[Eb] + R[Ei]*s
数据传送指令
- MOV相当于C语言的赋值'='
- mov S,D S中的字节传送到D中
机器级代码
- 指令集结构ISA
- 是机器级程序的格式和行为,定义了处理器状态、指令的格式,以及每条指令对状态的影响
- 机器级程序使用的存储器地址是虚拟地址
- 看上去是一个非常大的字节数组,实际上是将多个硬件存储器和操作系统软件组合起来
- 一些通常对C语言程序员隐藏的机器代码在IA32中是可见的:
- 程序计数器(在IA32中,通常称为“PC”,用%eip表示)指示将要执行的下一条指令在存储器中的地址。
- 整数寄存器:包含8个命名的位置,分别存储32位的数值,这些寄存器可以存储地址(对应C语言的指针)或整数数据,有的寄存器被用来记录某些重要的程序状态,其他的寄存器用来保存临时数据,例如过程的局部变量和函数的返回值。
- 条码寄存器:保存着最近执行的算术或逻辑指令的状态信息,他们用来实现控制或数据流中的条件变化。
- 浮点寄存器:一组浮点寄存器存放浮点数据
- 一条机器指令只执行一个非常基本的操作。机器代码只是简单地将存储器看成一个很大的、按字节寻址的数组。
代码示例
- 课本107页代码如下
int accum = 0;
int sum(int x, int y)
{
int t = x + y;
accum += t;
return t;
}
-
反汇编结果
gcc -c code.c
: -
汇编之后的代码
gcc -S code.c
(已删除“.”后的部分): -
前两条:
- pushl %ebp 将寄存器%ebp的内容压入程序栈
- movl %esp,%ebp 得到新栈低,将当前栈顶赋予栈低
-
后两条:
- popl %ebp过程调用结束,恢复旧栈低
- ret 子程序的返回指令
-
二进制文件可以用
od
命令查看,也可以用gdb的x命令查看。有些输出内容过多,可以使用more或less命令结合管道查看,也可以使用输出重定向来查看。od code.o | more
od code.o > code.txt
数据格式
C声明 | 汇编代码后缀 | 大小(字节) |
---|---|---|
char | b- 字节 | 1 |
short | w- 字 | 2 |
int | l- 双字 | 4 |
long | l- 双字 | 4 |
long long int | - | 8 |
char * | l- 双字 | 4 |
float | s- 单精度 | 4 |
double | l- 双精度 | 8 |
long double | t- 扩展精度 | 10/12 |
- IA32不支持64位整数运算
- 大多数GCC生成的汇编代码指令都有一个字符后缀,表明操作数的大小。
访问信息
-
一个IA32的中央处理器单元包含一组8个存储32位数值的寄存器。所有八个寄存器都可以作为16位(字)或32位(双字)来访问:
- %esi,%edi可以用来操纵数组
- %esp,%ebp用来操纵栈帧
- 64位的%rax,32位的%eax,16位的%ax,8位的%ah,%al都是独立的
-
操作数三种类型:
- 立即数,即常数值
- 寄存器,表示某个寄存器的内容
- 存储器,根据计算出来的地址(有效地址)访问某个存储器位置
-
有效地址的计算方式:
Imm(Eb,Ei,s) = Imm + R[Eb] + R[Ei]*s
控制
设置条件码
- 有两类指令(有8、16和32位形式),它们只设置条件码而不改变任何其他寄存器
CMP
指令根据它们的两个操作数之差来设置条件码TEST
指令的行为与AND
指令一样,除了它们只设置条件码而不改变目的寄存器的值。典型的用法是,两个操作数是一样的,或其中的一个操作数是一个掩码,用来指示哪些位应该被测试。
跳转指令及其编码
- JUMP指令,同样是汇编中常用的指令,根据不同的条件和符号位进行不同的跳转动作,具体见书128页。
- 跳转指令有几种不同的编码,最常用的是PC(程序计数器)相关的。
- 需要注意的是,jump分为直接跳转和间接跳转:
- 直接跳转:后面跟标号作为跳转目标
- 间接跳转:*后面跟一个操作数指示符
- 当执行与PC相关的寻址时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址。
过程
栈帧结构:
-
IA32程序用程序栈来支持过程调用。它包括将数据(参数和返回值)和控制从代码的一部分传到另一部分,另外还包括进入时为过程的局部变量分配空间,并在退出时释放空间。一般地,机器只提供转移控制到过程和从过程中转移出控制的简单指令,数据传递、局部变量的分配和释放必然通过程序栈实现。
-
机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。为单个过程分配的那部分栈称为栈帧。最顶端的栈帧以两个指针界定,寄存器%ebp为帧指针,寄存器%esp为栈指针。
-
栈指针%esp可以移动,帧指针%esp不变化,因而大多数信息的访问都是相对于帧指针的。如下图所示:
-
支持过程调用和返回的指令
- leave指令:为返回准备栈,它等价于如下代码:
movl %ebp,%esp
popl %ebp //恢复已保存的%ebp寄存器
- ret指令:从过程调用中返回。指的是从栈中弹出地址,并跳转到这个位置
- 寄存器使用惯例
- 调用者调用被调用者时,要求被调用者不能覆盖某个调用者稍后会使用的寄存器值。根据惯例,寄存器%eax、%edx、%ecx称为调用者保存寄存器。P调用Q时,Q可以覆盖这些寄存器而不会破坏任何P需要的数据(因为会恢复)。另一方面,寄存器%ebx、%esi等被划分为被调用者保存寄存器。要求Q必须在覆盖这些寄存器值之前,先把它们保存到栈中,并在返回前恢复它们。
转移控制:
- call指令:call指令有一个目标,即指明被调用过程起始的指令地址; call指令的效果是将返回地址入栈。并跳转到被调用过程的起始处。
- ret指令:ret指从栈中弹出地址,并跳转到这个位置;ret指令返回到call指令后的那条指令。
- leave指令:leave指令可以使栈做好返回的准备
- 寄存器使用惯例:%eax,%edx,%ecx 调用者保存寄存器;%ebx,%esi,%edi 被调用者保存寄存器;%ebp,%esp 保持寄存器。
- 保存某值的两种方式:由调用者保存,在调用之前就压进栈;由被调用者保存,在刚被调用的时候就压进栈,并在返回之前恢复。
函数调用栈信息的GDB命令
- backtrace/bt n
- n是一个正整数,表示只打印栈顶上n层的栈信息。
- n表一个负整数,表示只打印栈底下n层的栈信息。
- frame n
- n是一个从0开始的整数,是栈中的层编号。比如:frame 0,表示栈顶,frame 1,表示栈的第二层。
- 这个指令的意思是移动到n指定的栈帧中去,并打印选中的栈的信息。如果没有n,则打印当前帧的信息。
- up n
- 表示向栈的上面移动n层,可以不打n,表示向上移动一层。
- down n
- 表示向栈的下面移动n层,可以不打n,表示向下移动一层。
教材学习中的问题和解决过程
逆向工程的简单运用
- 分析如下:
- A题:数组本身其实没有二维三维之说,任何数组都可以说是线性结构,可以看成是从数组首位开始往后的线性排布,例如对于
A[R][S][T],A[i][j][k] 的位置是 A(,i*S*T+j*T+k,4)
。(A数组是int型所以占4个字节)。第一题,较为简单,暂时不涉及逆向工程。但这一题是求解第二题的关键所在 - B题:题目要求运用逆向工程技术,根据汇编代码,确定R、S和T的值。
- 首先,我们要根据汇编代码来求解此题,所以看懂汇编代码是关键。
- 其次,我们通过
movl $2772, %eax
可以得出,数组A占有R*S*T*4 = 2772
个字节,这是第一个等式。
movl 8(%ebp), %ecx ;get i
...
movl %ecx, %edx ;i -> %edx
sall $6, %edx ;i^6即64*i
subl %ecx, %edx ;64i-i=63i
- 上述几行汇编代码,因为我已经注释了,结合第一题答案,可以清楚的发现第二个等式:```S*T = 63```
movl 12(%ebp), %eax ;get j
leal (%eax, %eax, 8), %eax ;8*j+j -> j,最后 %eax = 9*j
- 上述几行汇编代码,因为我已经注释了,结合第一题答案,可以清楚的发现第三个等式:```T = 9```
- 综上,三个等式,可以解得该题的结果为:```R=11, S=7, T=9```
其他(感悟、思考等,可选)
- 首先,我觉得本周的学习任务是对上学期汇编的一个巩固,在原有我们所学的8086汇编知识的基础上得到了拓宽,这次我们所学的汇编主要是在32位机器上的。在寄存器个数没有变化的前提下,所能支持的位数翻了一倍,引入了
%eax, %ebx, %ebp...
等寄存器,操作指令也由原来的向左赋值变为向右赋值。虽然变化挺多,但本质是不变的一条机器指令只执行一个非常基本的操作以及汇编语言是近似机器语言,其造作指令与机器码一一对应(这在我们反汇编的实践中体现的很好)。 - 其次,在课后的练习以及家庭作业中,首次接触到了逆向工程的概念,即通过阅读汇编代码,得出原有C语言文件里的相关知识的工作。我也用该方法成功解决了几道题目,让我再次感受到了汇编语言的巨大魅力,虽然汇编语言可移植性不强,但专门用来对付某台机器上的C语言可是绰绰有余,可以帮我们挖掘到更多的信息。