3.1 历史观点

X86 寻址方式经历三代: 
1 DOS时代的平坦模式,不区分用户空间和内核空间,很不安全
2 8086的分段模式

3 IA32的带保护模式的平坦模式

 

3.2程序编码
对编程来说极为重要的两种抽象:
1.机器级程序的格式和行为,定义为指令集体系结构(ISA),定义了处理器状态,指令的格式,每条指令对状态的影响。
2.机器级程序使用的存储器地址是虚拟地址,提供的存储器模型看上去是一个非常大的数组。
 
PC的全称是program counter
程序计数器,是用来计数的,指示指令在存储器的存放位置,也就是个地址信息
 

 gcc -S xxx.c -o xxx.s 获得汇编代码,也可以用objdump -d xxx 反汇编(P107,108)

注意函数前两条和后两条汇编代码,所有函数都有,建立函数调用栈帧,应该理解、熟记。

注意: 64位机器上想要得到32代码:gcc -m32 -S xxx.c
MAC OS中没有objdump, 有个基本等价的命令otool 
Ubuntu中 gcc -S code.c (不带-O1) 产生的代码更接近教材中代码(删除"."开头的语句)

 

使用‘-c’命令行选项,GCC会编译并汇编该代码:gcc -o1 -c code.c 

产生目标代码文件code.o,为二进制格式文件。

*二进制文件可以用od 令查看,也可以用gdbx命令查看。

 

有些输出内容过多,我们可以使用 moreless命令结合管道查看,也可以使用输出重定向来查看

 

od code.o | more

 

od code.o > code.txt

 
Intel与ATT格式的不同:(P111)
  1. Intel代码省略了指示大小的后缀
  2. Intel代码省略了寄存器名字前的“%”符号
  3. Intel代码用不同的方式来描述存储器中的位置。
  4. 在带有多个操作数的指令情况下,列出操作数的顺序相反
 
3.3数据格式
 
3.4访问信息

esi edi可以用来操纵数组,esp ebp用来操纵栈帧。 只有根据 栈管理的标准惯例 才能修改这两个寄存器中的值。
对于寄存器,特别是通用寄存器中的eax,ebx,ecx,edx,大家要理解32位的eax,16位的ax,8位的ah,al都是独立的,我们通过下面例子说明:
假定当前是32x86机器,eax寄存器的值为0x8226,执行完addw $0x8266, %ax指令后eax的值是多少? 
解析:0x8226+0x826=0x1044c, ax16位寄存器,出现溢出,最高位的1会丢掉,剩下0x44c,不要以为eax32位的不会发生溢出.

操作数的三种类型:

      1.立即数,也就是常数值。

在ATT格式的汇编代码中,立即数的书写方式是“”后跟一个用标准C表示法表示的整数。任何一个能放进32 位字中的数值都可以用做立即数,不过汇编器在可能时会使用一个或两个字节的编码。

     2.寄存器

          表示某个寄存器的内容

 

     3.存储器

          根据计算出来的地址(有效地址)访问某个存储器的位置。

         有效地址的计算方式:  Imm(Eb,Ei,s) = Imm + R[Eb] + R[Ei]*s

 

 

MOV相当于C语言的赋值”=“,注意ATT格式中的方向:第一个是源操作数,第二个是目的操作数。

指令类:一类中的指令执行异议的操作,只不过操作数大小不同。

注意:不能从内存地址直接MOV到另一个内存地址,

        将一个值从一个存储器复制到另一个存储器中需要两个指令:1.第一条指令将源值加载到寄存器中。 2.将该寄存器值写入目的位置。

 

 区分MOVMOVSMOVZ      

MOV:将源操作数值复制到目的操作数中。

MOVS:将较小的源数据复制到一个较大的数据位置高位使用符号位扩展。

MOVZ:将较小的源数据复制到一个较大的数据位置高位使用零扩展。

 

栈是一个数据结构,可以添加或者删除数据,总是遵循“先进后出”原则。

栈顶:总是从栈的这端插入和删除元素。(栈顶元素的地址是所有栈中元素地址中最的)

 

指针就是地址。

间接引用指针:将该指针放在一个寄存器中,然后在储存器中使用这个寄存器。

局部变量保存在寄存器中。

 3.5算术和逻辑操作

 

指令类中的操作被分为四组:加载有效地址,一元操作数,二元操作数,移位。

 

加载有效地址指令 lean实际上是movl指令的变形。

指令形式实际上并没有引用存储器,它的第一个操作数其实是将有效地址写入到目的操作数

这条指令:1.可以为后面的存储器引用产生指针。2.简洁的表述普通算数操作。

目的操作数必须是一个寄存器。

 

*这里的操作数顺序与ATT格式的汇编代码中相反。

 

  • 一元操作:只有一个操作数,既是源又是目的。操作数可以是一个寄存器,也可以是一个储存器位置。
  • 二元操作:第二个操作数既是源又是目的。要注意源操作数是第一个,目的操作数是第二个。第一个操作数可以是立即数,寄存器或是存储器位置,第二个操作数可以是寄存器或者存储器位置。(不能两个操作数同时为储存器位置)
  • 移位操作:先给出移位量,然后第二项给出的是要移位的数值。注意移位操作移位量可以是立即数或%cl中的数 。移位操作的目的操作数可以是一个寄存器或者是一个存储器位置。

 

imull:“双操作数”乘法指令。从两个32位操作数产生一个32位乘积。
               补码乘法。一个参数必须在寄存器%eax中,另一个作为指令的源操作数给出。
mull:无符号数乘法。一个参数必须在寄存器%eax中,另一个作为指令的源操作数给出,乘积存放在寄存器%edx(高32位),%eax(低32位)中
idivl:有符号除法。寄存器%edx(高32位)和%eax(低32位)中的64位数作为被除数,除数作为指令的操作数给出。指令将商存储在寄存器%eax中,将余数存储在寄存器%edx中。
divl:无符号除法,通常实现将寄存器%edx设置为0.
 
 
3.6控制
条件码寄存器:它们描述了最近的算术或逻辑操作的属性。
常用:CF,ZF,SF,OF
两类只设置条件码而不改变任何其他寄存器:1.CMP:行为与SUB行为一致,但只改变条件码。2.TEST:行为与AND行为一致,但只改变条件码。
 
条件码常使用方法有三种:1.可以根据条件码的某一个组合,将一个字节设置为0或者1。      2.可以条件跳转到程序的某个其他部分。          3.可以有条件地传送数据。
SET:一类根据条件码的某个组合,将一个字节设置为0或者1的指令。这些指令的后缀表示不同条件而不是操作数大小。
SET指令根据t=a-b的结果设置条件码 。
 
跳转(jump)指令会导致执行切换到程序中的一个全新的位置。
在汇编代码中,这些跳转的目的地通常用一个标号指明。
 
jmp命令可以是直接跳转(跳转目标作为指令的一部分编码),也可以是间接跳转(跳转目标是从寄存器或存储器位置中读出的)
直接跳转:给出一个标号作为跳转目标。
间接跳转:‘*’的后面跟一个操作指示符。
 
条件跳转只能是直接跳转。
在汇编代码中,跳转目标用符号标号书写。
跳转编码方法:1,PC相关的:将目标命令的地址与紧跟在跳转指令后的那条指令的地址间的差作为编码。2.给出绝对地址,4字节直接指定目标。
 
当执行与PC相关的寻址时,程序计数器的值是跳转命令后面的那条命令的地址,而不是跳转指令本身的地址。
 
if-else 的汇编结构
t=test-expr;
if(!t)
    goto false;
 then-statement
    goto done;
false:
    else-statement
done:
 
do while:(标准表现P133)
每次循环,程序会执行循环体里 的语句,然后执行测试表达式。如果测试为真,则回去再执行一次循环。
 
while(P134)
与do-while不同的是,它对test-expr求值,在第一次执行body-statement之前,循环就可能终止。
 
do(P137)
 
switch:
switch(开关)语句可以根据一个整数索引值进行多重分支。
这里使用到跳转表:跳转表是一个数组,表项i是一个代码段的地址,这个代码实现当开关索引值等于i时程序应该采取的动作。
跳转表的优点:执行开关语句的时间与开关情况的数量无关。
 
 
3.7过程
机器用栈来传递过程参数,存储返回信息,保存寄存器用于以后恢复,以及本地存储。
为单个过程分配的那部分称为栈帧。、最顶端栈帧以两个指针来界定:寄存器%ebp为帧指针,而寄存器%esp为栈指针。栈指针可以移动,大多数信息访问都是相对于帧指针的。
 
 
Q会用栈帧来存放它调用的其他过程的参数。第一个参数放在相对于%ebp偏移量为8的位置处。剩下的参数存储在后续的4字节块中,所以参数i就在相对于%ebp的偏移量为4+4i的地方。
 
call指令有一个目标,即指明被调用过程起始的指令地址。(可以直接也可以间接)
call指令效果是将返回地址入栈,并跳转到被调用过程的起始处。
ret命令从栈中弹出地址,并跳转到这个位置。正确的使用这条命令,要使栈做好准备,栈指针要指向前面call指令存储返回地址的位置。
寄存器%eax可以用来返回值。
 
程序寄存器是唯一能被所有过程共享的资源。
根据惯例,寄存器%eax,%edx,和%ecx被划分为调用者保存寄存器,寄存器%edx,%esi,和%edi被划分为被调用者保存寄存器。
 
GCC坚持一个X86编程指导方针,也就是一个函数使用的所有栈空间必须是16字的整数倍。采用这个原则是为了保证访问数据的严格对齐。
栈规则提供了一种机制,每次函数调用都有它自己私有的状态信息(保存的返回位置,栈指针和被调用者保存寄存器的值)存储。有需要的话还可以提供局部变量的存储。
 
3.8数组分配与访问
对于T A[N]
有两个效果:1.它的存储器中分配一个L▪N字节的连续区域(L是数据类型T的大小)。 用Xa来表示起始位置。2.引入了标识符A;可以用A作为指向数组开头的指针,这个指针的值就是Xa。
 
如果p是一个指向类型为T的数据的指针。p的值为xp,那么表达式p+i的值为xp+L▪i
 
单操作数的操作符&和*可以产生指针和间接引用指针。就是说,对于一个表示某个对象的表达式Expr,&Expr是给出该对象地址的一个指针。对于一个表示地址的表达式AExpr,*AExpr是给出该地址处的值。
因此,Expr和*&Expr等价。数组引用A[i]等同于*(A+i)。
 
计算同一个数据结构中两个指针的差,结果值是以数据类型大小后的值。
 
多维数组声明如下: T D[R][C]
它的数组元素D[i][j]的存储器地址为:&D[i][j]=xD+L(C*i+j)
 
寄存器溢出:没有足够多的寄存器来保存需要的临时数据,因此编译器必须把一些局部变量放在存储器中。
 
3.9异质的数据结构
c语言提供两种不同类型的对象来创建数据结构类型的机制:
结构(structure):用关键字struct声明,将多个对象集合到一个单位中。
                                结构的各个字段的选取完全是在编译时处理的。机器代码不包含关于字段声明或字段名字信息。
联合(union):用关键字union声明允许用几种不同的类型来引用一个对象。
 
数据对其限制简化了形成处理器和存储器系统之间接口的硬件设计。
分配处理器的库列程的设计必须使它们返回的指针满足运行机器最糟糕情况的对其限制,通常是4或8。
 
3.10综合:理解指针
指针隐射机器代码的关键原则。
  • 每一个指针都对应一个类型。
  • 每一个指针都有一个值。
  • 指针用&运算符创建
  • 数组与指针紧密联系
  • 将指针从一种类型强制转换为另一种类型只改变它的类型而不改变它的值。
  • 指针也可以指向函数
 
3.11应用:使用GDB调试器

命令

参数含义

说明

backtrace

最内层的n个函数栈帧

-n 最外层n个函数栈帧

栈帧回溯

frame

栈帧号或者内存地址

选定栈帧,不带参数时显示栈帧简要信息

up

栈帧号

选定栈帧上移

down

栈帧号

选定栈帧下移


显示栈帧

      backtrace 命令可以在遇到断点而暂停执行时显示栈帧。此外,backtrace 的别名还有 where 和 info stack。

      (gdb) backtrace

      显示所有栈帧。

      (gdb) backtrace N

      只显示开头 N 个栈帧。

      (gdb) backtrace -N

      只显示最后 N 个栈帧。

      (gdb) backtrace full

      (gdb) backtrace full N

      (gdb) backtrace full -N

      不仅显示backtrace,还有显示局部变量。

      显示栈帧之后,就可以看出程序在何处停止(即断点的位置),以及程序的调用路径。

 
 
 
 
最后一部分参考了这个: