博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

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

教材学习内容总结

3.1历史观点

  • X86 寻址方式经历三代:
  1. DOS时代的平坦模式,不区分用户空间和内核空间,很不安全
  2. 8086的分段模式
  3. IA32的带保护模式的平坦模式
  • Linux使用平坦寻址方式,使程序员将整个存储空间看做一个大的字节数组。

3.2程序编码

  • ISA:指令集体系结构,机器级程序的格式和行为,它定义了处理器状态、指令的格式以及每条指令对状态的影响。
  • 程序计数器(通常称为PC,用%eip表示),指示将要执行的下一条指令在存储器中的地址。
  • 整数寄存器文件:存储地(对应于C语言的指针)或整数数据。
    条件码寄存器:保存着最近执行的算数或逻辑指令的状态信息,用来实现控制或者数据流中的条件变化。
  • 浮点寄存器:用来存放浮点数据。

编译过程:

  • 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 反汇编
  • 建立函数调用栈帧的汇编代码:
pushl   %ebp 将寄存器%ebp中的内容压入程序栈
movl    %esp,%ebp  将%ebp中的内容放入寄存器%esp
......
popl    %ebp
寄存器%ebp中内容出栈
ret 返回结果

注意:

  1. 64位机器上想要得到32代码:gcc -m32 -S xxx.c
  2. MAC OS中没有objdump, 有个基本等价的命令otool
  3. Ubuntu中 gcc -S code.c (不带-O1)产生的代码更接近教材中代码(删除"."开头的语句)
  4. 找到程序的字节表示:(gdb) x/17xb sum
  5. 二进制文件可以用od命令查看,也可以用gdb的x命令查看。有些输出内容过多,我们可以使用 more或less命令结合管道查看,也可以使用输出重定向来查看od code.o | more od code.o > code.txt

格式注解:

  • 开头的行都是指导汇编器和链接器的命令,gcc -S 产生的汇编中可以把 以‘.’开始的语句都删除了再阅读
  • Linux和Windows的汇编格式的区别:
  1. ATT格式(Linux下的汇编格式)&Intel格式(Windows的汇编格式);
  2. Intel代码省略了指示大小的后缀。我们看到指令mov而不是movl;
  3. ntel代码省略了寄存器名字前的%,使用的是esp而不是%esp;
  4. Intel代码用不同方式来描述存储器中的位置:例如,是DWORD PTR [ebp+8]’而不是'8(%ebp)

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位的不会发生溢出.

寻址方式

  • 根据操作数的不同类型,寻址方式可分为以下三种:
  1. 立即数寻址方式:操作数为常数值,写作$后加一个整数。
  2. 寄存器寻址方式:操作数为某个寄存器中的内容。
  3. 存储器寻址方式:根据计算出来的地址访问某个存储器的位置。
  • 寻址模式:一个立即数偏移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

注意

  • 栈顶元素的地址是所有栈中元素地址中最低的,后进先出;
  • 指针就是地址;局部变量保存在寄存器中。

3.5算术和逻辑操作

加载有效地址

  • leal,从存储器读数据到寄存器,而从存储器引用的过程实际上是将有效地址写入到目的操作数。
  • 目的操作数必须是一个寄存器。

一元操作和二元操作

  • 一元操作:只有一个操作数,既是源又是目的,可以是一个寄存器或者存储器。
  • 二元操作:第二个操作数既是源又是目的,两个操作数不能同时是存储器。

移位

  • 先给出位移量,然后是位移的数值,可进行算数和逻辑右移。
  • 移位操作移位量可以是立即数或%cl中的数。

3.6控制

条件码

  • 描述最近的算数或者逻辑操作的属性,可以检测这些寄存器来执行条件分支指令。

  • CF:进位标志,最近操作使高位产生进位,用来检测无符号操作数的溢出

  • ZF:零标志,最近操作得出的结果为0

  • SF:符号标志,最近操作得到的结果为负数

  • OF:溢出标志,最近操作导致一个补码溢出-正溢出或负溢出。

注意

  • leal不改变条件码寄存器
  • CMP与SUB的区别:CMP也是根据两个操作数之差设置条件码,但只设置条件码而不更新目标寄存器
  • 有条件跳转的条件看状态寄存器(教材上叫条件码寄存器)

访问条件码的读取方式

  1. 根据条件码的某个组合,将一个字节设置成0或1;
  2. 跳转到程序某个其他的部分;
  3. 有条件的传送数据。
  • SET指令根据t=a-b的结果设置条件码

跳转指令及其编码

  • 控制中最核心的是跳转语句:
  1. 有条件跳转(实现if,switch,while,for)
  2. 无条件跳转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语句的关键步骤是通过跳转表来访问代码位置,使结构更加高效。

3.7过程

  • 数据传递、局部变量的分配和释放通过操纵程序栈来实现。

  • C语言过程调用机制使用了栈数据结构提供的后进先出的内存管理原则。

  • 函数调用的栈帧结构用来传递参数、存储返回地址、保存寄存器及本地存储

  • P(调用者)调用Q(被调用者),则Q的参数放在P的栈帧中。调用Q是P的返回地址被压入栈中,形成P的栈帧的末尾。返回地址是程序从Q返回时应该继续执行的地方。Q的栈帧从保存的帧指针如%ebp开始,后面是保存的寄存器值。

栈帧结构

  • 为单个过程分配的栈叫做栈帧,寄存器%ebp为帧指针,而寄存器指针%esp为栈指针,程序执行时栈指针移动,大多数信息的访问都是相对于帧指针。
  • 栈向低地址方向增长,而栈指针%esp指向栈顶元素。

转移控制

  • call:目标是指明被调用过程起始的指令地址,效果是将返回地址入栈,并跳转到被调用过程的起始处。
  • ret:从栈中弹出地址,并跳转到这个位置。
  • 函数返回值存在%eax中
  • call和ret指令的一般形式:
指令 描述
call Label 过程调用
call *Operand 过程调用
ret 从过程调用中返回

栈帧结构

  • 为单个过程分配的栈叫做栈帧,寄存器%ebp为帧指针,而寄存器指针%esp为栈指针,程序执行时栈指针移动,大多数信息的访问都是相对于帧指针。
  • 栈向低地址方向增长,而栈指针%esp指向栈顶元素。

转移控制

  • call:目标是指明被调用过程起始的指令地址,效果是将返回地址入栈,并跳转到被调用过程的起始处。
  • ret:从栈中弹出地址,并跳转到这个位置。
  • 函数返回值存在%eax中

寄存器使用惯例

  • 程序寄存器是唯一能被所有过程共享的资源,调用者保存寄存器 和 被调用者保存寄存器是分开的,对于哪一个寄存器保存函数调用过程中的返回值要有统一的约定。

3.8数组分配和访问

  • 对数据类型T和整型常数N的声明5:T A[N];
  • 指针运算:单操作数的操作符&和*可以产生指针和间接引用指针。leal指令用来产生地址,movl用来引用存储器

嵌套的数组

  • 定长数组(程序要用一个常数作为数组的维度或缓冲区大小时最好使用#define声明将这个常数与一个名字联系起来,后面使用该名字代替常数的数值)
  • 变长数组(允许数组的维度是表达式)

3.9 异质的数据结构

  • 两种结合不同类型的对象来创建数据类型的机制:结构(structure)、联合(union)
  1. 结构:结构的各个字段的选取是在编译时处理的,机器代码不包含关于字段声明或字段名字的信息
  2. 联合:允许以多种类型来引用一个对象,是用不同的字段来引用相同的存储器块。一个联合的总的大小等于它最大字段的大小。联合还可以用来访问不同数据类型的位模式。
  • 数据对齐:某种类型对象必须是某个K值(通常是2、4或8)的倍数。这种对齐限制简化了形成处理器和存储器系统之间接口的硬件设计

3.10 综合:理解指针

  • 每个指针都对应一个类型
  • 每个指针都有一个值
  • 指针用&运算符创建
  • 运算符*用于指针的间接引用
  • 数组与指针紧密联系
  • 将指针从一种类型强制转换成另一种类型,只改变它的类型而不改变它的值
  • 指针也可以指向函数

3.11 关于栈帧的gdb命令

  • backtrace/bt:打印当前的函数调用栈的所有信息。后面加n或-n表示打印栈顶上n层(或者下n层)的栈信息。
  • frame n:n为栈中的层编号,从0开始,类似C语言中数组的下标。移动到n指定的栈帧中去,并打印选中的栈的信息。如果没有n,则打印当前帧的信息。
  • up n:表示向栈的顶移动n层。
  • down n:表示向栈底移动n层。

课后部分习题:

当我们调用汇编器的时候,下面代码的每一行都会产生一个错误信息,解释每一行都是哪里出了错?

  • movb $0xf,(%bl)————(%bl)不能做为寄存器地址
  • movw (%eax),4(%esp)————目的操作数与源操作数不能都是存储器
  • movb %si,8(%ebp)————指令后缀与寄存器地址不匹配

xp,yp,zp分别存储在相对于寄存器%edp中地址值偏移8、12、16的地方。试写出与以下代码等价的C语言代码。

movl 8(%ebp),%edi
movl 12(%ebp),%edx
movl 16(%ebp),%ecx
movl (%edx),%ebx
movl (%ecx),%esi
movl (%edi),%eax
movl %eax,(%edx)
movl %ebx,(%ecx)
movl %esi,(%edi)
  • 代码如下:

根据汇编代码,补充C语言代码(x,y,z分别存储在相对于寄存器%edp中地址值偏移8、12、16的地方).

movl 12(%ebp),%eax
xorl 8(%ebp),%eax
sarl $3,%eax
notl %eax
subl 16(%ebp),%eax
  • C语言代码:

已知下列C语言代码:

void cond(int a,int *p)
{
    if(p&&a>0)
        *p +=a;
}

按照与汇编代码等价的C语言goto版本,写一个与之等价的C语言代码。

  • 相关c语言代码:

对```call next

next:
popl %eax```的理解。

  • 可以理解为call指令的效果是将返回地址入栈,也就是call后面指令的地址,即popl %eax,而%eax的值又被设置为popl指令的地址,整个过程是顺序执行的而并没有发生跳转,所以无需ret弹出。

练习

  • 输入代码

  • 使用gcc -S -o main.s main.c -m32编译成汇编代码

  • 删除gcc产生代码中以"."开头的编译器指令

分析

main函数

  • 保存进入main函数前的栈底指针%ebp,接着让%ebp与%esp指向相同位置,即建立main函数运行的空栈

    pushl %ebp
    movl %esp, %ebp
    pushl $8将立即数压入栈
    
  • 即相当于

    subl    $4,%esp
    movl    $8,(%esp)
    
  • call调用f函数

f函数

  • 开始类似main函数,保存main函数的栈底,建立f函数空栈

  • pushl $8将%esp中的8存入栈中,相当于

    subl    $4,%esp
    movl    $8,(%esp)
    
  • call调用g函数

g函数

  • 保存f函数的栈底,建立f函数空栈
  • movl 8(%ebp),%eax取出当前ebp往上8个字节处的内容8
  • addl $7,%eax将%eax与$1相加
  • popl %ebp弹出栈顶至%ebp,恢复%eip

返回

  • g函数中ret返回f函数中调用位置
  • f函数中ret返回main函数中调用位置
  • addl $14,%eax对%eax加9
  • leave为返回准备栈
  • ret结束

教材学习中的问题和解决过程

leaq作为加载有效地址指令,实际上是movq的变形。那么leaq与movq的用法有什么不同?

  • LEA指令的功能是取偏移地址,例如LEA AX,[1000H],作用是将源操作数[1000H]的偏移地址1000H送至AX;
  • MOV指令的功能是传送数据,例如MOV AX,[1000H],作用是将1000H作为偏移地址,寻址找到内存单元,将该内存单元中的数据送至AX。

while循环有两种翻译方式,这两种翻译方式分别什么时候使用呢?

  • 较低优化等级编译时(-Og)会采用跳转到中间的策略,较高优化等级时采用guarded-do策略

上周考试错题总结

  • 解析:嵌入式开发中,通过nfs系统把Ubuntu虚拟机的一个目录映射成ARM实验箱的Linux系统的一个目录进行调试是一个惯用法,程序调试没有问题了,再烧写到实验箱的Linux的系统中,这样实验箱重启了程序也可以用了。并且,ARM实验箱的IP修改要重新烧录Linux系统,Windows 宿主机,Ubuntu虚拟机的IP比较容易修改,所以我们在超级终端中通过ifconfig查看ARM实验箱的IP,把Windows 宿主机,Ubuntu虚拟机的IP改成和ARM实验箱同一个网段。

  • 出错原因:忘记了如果返回值小于0,则表示出现了错误.如果错误为EINTR说明读是由中断引起 的, 如果是ECONNREST表示网络连接出了问题。

我的体会

本章主要是学习了机器内执行程序的时候所发生的动态变化。要究动态,就必须了解静态的内部“参量”;有前面打下的基础,才有后面学得懂、学得透的可能性。本章学习需要大量的上学期汇编语言的基础,才能保证流畅阅读基础的汇编代码,而不用时时刻刻“查字典”,但实际上在上学期学习中也没有做的很好,所以在一定程度上加大了学习的难度。