深入理解计算机系统——汇编基础

426 阅读11分钟

自己复习的时候,把题目写一下,会更好的理解一下 根据我的经验,画栈帧图的时候,ebp``esp画在图的右侧比较好——栈帧从上到下地址是递减的,右侧就是表示的低地址

寄存器

32位:

  • 数据寄存器

eax``ebx``ecx``edx
eax通常作为函数的返回值

  • 指针寄存器

esp指向栈顶
ebp指向栈底

  • 变址寄存器

esi``edi

  • 段寄存器

es
cs代码段寄存器
ss堆栈段寄存器
ds数据段寄存器
fs
gs

  • 指令指针寄存器

eip存储下一条指令的地址

  • 标志寄存器

EFlags
注意:局部变量保存在寄存器中
上面的e就是扩展,16位的变成了32位
image.png

数据格式

字节b——1字节
字w——2字节
双字l——4字节

立即数:一个数加上$符号——$12
直接寻址:如果是寄存器,那么操作数就是寄存器存储的数——%eax;如果是一个数(地址)的话,操作数就是地址指向的那块内存的数据——0x324
间接寻址:寄存器里的数据指向的内存数据——(%eax)
其他的形式:

2(%eax)%eax里面的值加2的和作为地址,该地址指向的内存数据
3(%eax,%ebx)两个寄存器里面值相加、再加上3的和作为地址,该地址指向的内存数据
3(%eax,%ebx,2)%eax+%ebx*2+3的和作为地址,指向的内存数据

mov操作

movd移动一个字节
movw移动一个字
movl移动2字
其他的汇编指令和这类似,下文不再论述

movl $123 %eax//立即数存入eax
movl 0x123 %eax//0x123地址处的数存入eax,比如0x123地址处的内存数据为1,那么eax就存1
movl (%ebx) %eax//ebx里面的数作为地址,把该地址处的数存入eax
//剩下的可以根据上面的数据格式就可以懂了
  • movs符号扩展——补符号位

movsbw一字节符号扩展到字, movsbl一字节符号扩展到两字 movswl字符号扩展到两字

  • movz零扩展——补0

同上,只不过是零扩展。

push,pop

  • push压栈

pushl %eaxesp先减4,然后把eax的值压入栈顶

  • pop出栈

popl %eax把栈顶的数移入eax寄存器中,esp加4

pushl %ebp
movl %esp,%ebp

所有函数的头两条指令都是为了初始化自己的堆栈空间

call,ret

函数调用指令,调用该地址
call 0x12345
执行该操作会做出两个动作
把当前的%eip的值压栈,然后把0x12345放入eip
为什么要这样呢?
eip压栈是保存旧的函数栈帧,因为eip保存的是计算机要执行的下一条指令,使函数调用完成之后,可以继续回到原来的函数栈帧中继续执行代码。

执行指令ret——函数返回指令
执行该指令之后,把call压入的eip的数据(当前栈顶的第一个存储单元),重新弹回eip中
*表示不存在实际对应的指令
image.png
eip不能被程序员直接修改,只能通过专用指令(call,ret,jmp)等间接修改

把程序计数器放在整数寄存器的唯一方法

	call next
next:
	popl %eax

leave,enter

leave指令用来撤销函数栈帧的,等价于下面两条指令:

movl %ebp,%esp
popl %ebp

enter指令用来建立函数栈帧,等价于下面两条指令:

pushl %ebp
movl %esp,%ebp

算术和逻辑操作

leal加载有效地址,其实和movl的意思差不多,但是它根本就没有引用存储器。
leal的用法

leal -8(%ebp) ,%eax它表达的意思是把ebp-8数据的地址赋给eax——eax中的值就是ebp-8
movl -8(%ebp) ,%eax它表达的意思是把ebp-8处的数据赋给eax——eax中的值(ebp-8)

一元操作

inc x自加1 x+1
dec x自减1 x-1
neg x变负 -x
not x取补 ~x

二元操作

add
sub
xor异或
or
and

移位操作

sal``shl左移
sar算术右移——补符号位
shr逻辑右移——补0

特殊的算术操作

imull S这个发现只有一个操作数,S乘以eax寄存器中的值,结果是有符号的64位的,前32位放在edx中,后32位放在eax中。
mull S和上面的一样,只不过是无符号64位的。说明i表示的是有符号
imull S,D这个就是普通的乘法,S*D->D,32位的
cltd转为4字(64位)——把eax寄存器中的值转成64位,前32位存在edx,后32位存在eax中。
idivl S有符号除法,edx:eax组成的64位数除以S,商存在eax中,余数存在edx中
divl S无符号除法,和上面一样

控制

条件码

image.png
下面两个指令只设置条件码
image.png
对应cmp指令,如果两个数相等,指令会将零标志设置为1

访问条件码

常用的三种方法:

  1. 根据条件码的组合,将一个字节设置为0或者1,2
  2. 可以条件跳转到程序的某个其他的部分
  3. 可以条件的传送数据

image.png
set指令的后缀不表示操作的数大小

跳转

跳转指令,跳转到标志的地方
image.png
上面这个就是跳转到.L1的位置

jmp无条件跳转
jmp *%eax用寄存器eax中的值作为跳转目标
jmp *(%eax)寄存器eax里面的值作为地址,把改地址指向的内存空间的数据当作跳转目标

image.png

image.png
当执行与PC(当前指令的下一条指令)相关的寻址的时,程序计数器的值是跳转指令后面的那条指令的地址,而不是跳转指令本身的地址。
image.png

jle后面的地址是怎么得到的呢?很简单——PC的值加上0d

很好的例题

image.png

翻译条件分支

咱就是说,条件跳转是真的狗,具体怎么狗的,看下面这道题目:
image.png
注意:以下解释,如果读者看不懂,自己做一下实验即可。
总结:每对if-else的第一个条件,其实是跳转到else执行的。(也就是说,第一条跳转指令是跳转到else的地方)这种情况初学者不是很了解。(先进行不满足条件的跳转
其他是对源代码进行改写,变成goto语句,这样就方便理解了。

  1. eax寄存器存x
  2. edx寄存器存y
  3. x与-3的关系,进行设置条件码,怎么设置的咱就别管理——一定注意是后面与前面的数比较
  4. 该汇编是根据条件码进行跳转,我们不管条件码。结合3,它的意思是,如果x>=-3,跳转到.L2。但是,实际的C语言中,没有该条件。该语句其实是执行的else语句。你品,你细品。

.L2之后就是正常跳转,因为这个是一组条件语句
13,14:x>2跳转到.L5
如果不满足条件:执行15条语句,该语句其次才对应着c语言的第一条语句——val=x^y

上面是把条件中的else中的代码完成了
从第5条汇编语句开始就是执行的if里面的代码
下面这个语句一看是比较,。。。。。。。,这咋又是一组if-else
5,6:x<=y,跳转到.L3。也就是说,y<x执行的才是if语句
下面就不讲了,没有什么难的了。期末一定会考的哦

image.png

循环

汇编中没有循环,而是用条件+跳转组成实现的。
其他的循环会先转换成do-while的形式,然后再编译成机器代码。
转换成goto形式比较好理解

条件传送指令

image.png
解释一下下面语句
image.png

switch

image.png

p146页,第5句话表示的跳转到switch的默认位置 第6句话,表示间接跳转,可以看数组那一节 第3,4句话,表示把n值减100,目的就是为了把传入的值(变成索引之后)控制在0~6范围内

image.png

.L7中的7表示数组中有7个标号(有的值可能不存在) 画红线的地方,表示真实的下标(对应c语言的)——当然这是注解,实际是看不见的 根据上面所示101这个是不存在的,因为是默认跳转,104、106公用一个跳转,说明其中一个不存在break语句

image.png

函数堆栈

什么叫做栈帧?

单个函数调用操作所使用的栈部分被称为栈帧结构(以后画右侧,看文章最前面的说明image.png

对于32位的x86cpu来说,通过堆栈来传参的方法是从右到左的——即在准备调用的时候,从右到左进行参数的压栈操作
如:
swap(a,b)pushl bpushl a

64位的不同,这里不做讨论
函数的返回值用eax进行保存的,如果要返回多个值,那么它保存的就是那块空间的地址

我们知道vc6.0(骨灰级编译器)在声明变量的时候,全部的变量要声明在最前面?
为什么会这样呢?
因为早期的编译器不够智能,不能智能的预留空间,所以要求程序员在最前面声明,这样在建立函数堆栈的时候,就会一个一个进行空间的申请。

我们需要确定在一个函数调用其他函数的时候,被调用者不会修改或者覆盖调用者所用到的寄存器内容。 因此InterCPU采用统一惯例 惯例指明: 寄存器eax``edx``ecx的内容由调用者自己负责——也就是说,被调用者,可以随意的修改 寄存器ebx``esi``edi的值必须由被调用者来负责——也就是说,被调用者使用这些寄存器的时候,必须先进行保存,退出时进行恢复。当然寄存器ebp``esp也遵守这个

看下面这份汇编代码:
image.png
我们发现栈帧开辟了24字节,但是我们只使用了16个字节,还有8个字节永远不会使用
为什么呢?
gcc坚持一个函数使用的所有栈空间必须是16字节的整数倍,(24虽然不是整数倍,但是看是所有,还要加上保存的ebp和返回值,这两个总共8字节,也就是32)

一个函数的栈帧包含这几个部分:

  1. 建立部分——初始化栈帧
  2. 主体部分——执行过程的计算
  3. 结束部分——恢复栈的状态,以及过程返回

对于释放栈帧可以用leave的方法,也可以用一个或者2个popl指令。这两种都可以,看见知道是什么意思就行。

递归的过程

直接看题目解释:
image.png
这里的条件判断不会那么狗,就是很直接的。
下面对汇编语句进行解释:

  1. ebx=x
  2. eax=0
  3. 检测
  4. 如果x==0,跳转到.L3
  5. eax=x
  6. eax=x>>1
  7. 该行是准备参数的过程,把eax的值放入栈顶
  8. 递归开始
  9. edx=x
  10. edx=x&1
  11. eax为返回值,eax=(edx+eax)=(x&1)+rv

数组

直接上例子:结果是指针,存入eax中;结果是整型,存入ax中。
image.png

关键部分解释(其实是复习一下c语言的知识): 指针加一个数,那么该指针的值为:地址加上这个数乘以类型的大小 例: short p[5]p的地址为xp,那么p+i的值就是xp+2*i

二维数组:
记住这个公式:

T D[R][C]

&D[i][j]=xd+L(C*i+j)//L是T的类型

数据结构

这里主要说的是:结构体、联合体

结构体

我们访问结构体中的字段的时候,用的是结构体的地址加上适当的偏移
image.png
比如上面这个%eax*4就是找到数组第i个元素相对于数组首元素的偏移量,加上8就是相对于结构体首地址的偏移量,最后加上%edx就是最终的地址。
看看这个例子:
image.png

联合

在使用联合的时候主要注意将各种不同大小的数据类型结合在一起的时候,字节顺序的问题是很重要的

其实和结构体差不多。直接做个例题就行,看下面的例题:
image.png
image.png

关于数据的对齐,我在C语言结构体code-child.cn/post/192中已经进行了说明。
通常在汇编代码中,会有这样的命令:.align 4表示遵守4字节对齐的限制。

使用GDB调试

除了下面表中的,还可以看我之前写的文章code-child.cn/post/148
也可以在线查找进行调试:

  1. gdb 程序——开始调试
  2. help all

image.png

缓冲区溢出

我们在vs下面使用scanf``gets``strcpy等库函数的时候,经常说他们是不安全的,为什么这么说呢,因为它们不进行检查栈帧空间的大小,一直写或者进行其他操作,就会越过栈帧的给它们开辟的空间,导致发生可怕的操作。
看下面的练习题
image.png
image.png