深入理解计算机系统-第三章-程序的机器级表示-3.4

上一篇博客我们讲了在汇编语言中,如下的几个处理器状态是可见的:

一、程序计数器(在 IA32 中通常称为 PC,用 %eip 表示):指示将要执行的下一条指令在存储器中的地址。

二、整数寄存器文件:包含8个命名的位置,可以存储一些地址或者整数的数据。有的用来记录某些重要的程序状态,有的则用来保存临时数据。

三、条件码寄存器:保存最近执行的算数或逻辑指令的状态信息,它们用来实现控制或数据流中的条件变化,比如用来实现 if 和 while 语句。

四、浮点寄存器:存储浮点数。

这里我们要讲的就是第三个整数寄存器,在 32 位 CPU 中包含一组 8 个存储 32 位值的寄存器。这些寄存器用来存储整数数据或指针。下图是 IA32 的整数寄存器:

在这里插入图片描述

上述八个寄存器主要功能如下:

%eax,可存放一般数据,而且可作为累加器使用;
%ebx,可存放一般数据,而且可用来存放数据的指针(偏移地址);
%ecx,可存放一般数据,而且可用来做计数器,常常将循环次数用它来存放;
%edx,可存放一般数据,而且可用来存放乘法运算产生的部分积,或用来存放输入输出的端口地址(指针);
%esi,可存放一般数据,还可用于串操作中,存放源地址,对一串数据访问;
%edi,可存放一般数据,还可用于串操作中,存放目的地址,对一串数据访问;
%esp,用于寻址一个称为堆栈的存储区,通过它来访问堆栈数据;
%ebp,可存放一般数据,用来存放访问堆栈段的一个数据区,作为基地址;
  在大多数情况下,%eax、%ecx、%edx、%ebx、%esi、%edi等6个寄存器可以看做通用寄存器,对它们的使用没有限制;%esp、%ebp两个寄存器保存着指向程序栈中重要位置的指针,只有根据栈管理的标准惯例才能修改这两个寄存器中的值。

这8个寄存器都可以作为16位(字)或32位(双字)来访问。字节操作指令可以独立的读或者写%eax、%ecx、%edx、%ebx等4个寄存器的2个低位字节,因为%ax、%cx、%dx、%bx这4个16位寄存器又可分别分成ah,al ;bh,bl;ch,cl;dh,dl的8位寄存器。

这里大家也只需要有个眼熟就好了,后面我们将对这个8个寄存器进行详细讲解。

操作数指示符

我们知道大多数指令都有一个或多个操作数(operand),指示出执行一个操作中要引用的源数据值,以及放置结果的目标位置。下图是 IA32 支持的多种操作数格式:

在这里插入图片描述

上图我们可以看出源数据值可以是常数形式给出,或者是从寄存器或存储器中读出。而结果可以存放在寄存器或存储器中。我们将不同的操作数分为如下三种类型:

①、立即数(immediate):书写方式是$符号后跟一个标准C表示的整数,比如$52,$0x1F等等。任何能放进一个32位的字里面的数值都可以做立即数。

②、寄存器(register):它表示某个寄存器的内容,可以是8个32位寄存器中的一个(比如%eax),也可以是8个16位寄存器中的一个(比如%ax),还可以是8个单字节寄存器寄存器(比如%al)。上图是用Ea来表示任意寄存器a,用引用 R[Ea]来表示它的值。

③、存储器(memory):它会根据计算出来的地址(通常称为有效地址)来访问某个存储器位置。我们将存储器看成一个很大的字节数组,用符号Mb[Addr] 表示对存储在存储器中从地址 Addr 开始的 b 个字节值的引用。上图省略了下方的 b.

从上图我们知道,第一行是立即数,第二行则是寄存器,剩下的全部是存储器。其中最后一行存储器语法 Imm(Eb,Ei,s),表示的是最常用的形式,分为四个部分,

一、Imm 是立即偏移数

二、Eb 是基址寄存器

三、Ei 是变址寄存器

四、s 是比例因子,必须是 1、2、4或8

然后有效地址计算公式为: Imm + R[Eb]+R[Ei]s。比如对于2(%esp,%eax,4)这个操作数来讲,它代表的是内存地址为2+%esp+4%eax的存储器区域的值。

在这里插入图片描述

数据传送指令

操作数符号的通用性使得一条简单的传送指令能够完成许多机器中要好几条指令才能完成的功能。
源操作数制定一个数,它可以是立即数,可以存放在寄存器中,也可以存放在存储器中。
目的操作数制定一个位置,可以是寄存器,也可以是存储求地址。
IA32中传送指令的两个操作数不能都指向存储器的位置,就是将一个值从存储器的位置拷到另一个存储器的位置需要两条指令。第一条指令从源值加载到寄存器中,第二条将该寄存器写入目的位置

1         movl $0x4050,%eax          立即数--寄存器
2         movl %ebp,%esp             寄存器--寄存器
3         movl (%edi,%ecx),%eax      存储器--寄存器
4         movl $-17,(%esp)           立即数--存储器
5         movl %eax.-12(%ebp)        寄存器--存储器

movb只传送一个字节,movw指令传送两个字节
在这里插入图片描述
movsbl和movzbl指令负责拷贝一个字节,并设置目的操作数中其余的位,movsbl指令的源操作数是单字节的,它执行符号扩展到32位(也就是将高24位设置为源字节的最高位),然后拷贝到双字的目的中。类似的,movzbl指令源操作数是单字节的,在前面加24个0扩展到32位,将结果拷贝到双字的目的中
例子

初始假设 %dh=8,%eax=98765432(32位)
1        movb %dh,%al                 %eax=9876548D
2        movsbl %dh,%eax            %eax=FFFFFF8D
3        movzbl %dh,%eax            %eax=0000008D

movb指令不改变其他三个字节。
根据源字节的最高位,movsbl指令将其他三个字节设置为全1或者全0.
movzbl指令无论如何都是将其他三个字节设置为全0

MOV指令

mov指令的作用是将源操作数S中的数据复制到目的操作数D中,mov指令有一个数据格式和两个操作数,因此一般的形式为[movx S D]。其中x为数据格式,S为源操作数,D为目的操作数。

这里举一个简单的例子,比如我们有一条指令为movl %edx %eax。那么它的执行过程就如下图所示。

在这里插入图片描述

可以看到,在指令执行之后,%edx寄存器当中的内容会被复制到%eax寄存器。需要一提的是,mov指令可以在后面加上任何数据格式,比如上面这一过程中,数据格式则为四个字节,也就是双字。因此不难推断出,我们还可以使用movb和movw去复制一个字节或者两个字节。

movs指令

movs指令的作用是将源操作数S中的数据做符号扩展后,再复制到目的操作数D中,movs指令有两个数据格式和两个操作数,因此一般的形式为[movsxy S D]。其中x、y为数据格式,S为源操作数,D为目的操作数。其中x、y的组合一共有三种,分别是bw、bl、wl,这三个组合代表的意思分别是单字节到双字节,单字节到双字以及双字节到双字。

这里LZ依然举一个例子,对于指令movswl %dx %eax来讲,它的作用如下图所示。
在这里插入图片描述

这里为了可以看出符号位的扩展,因此LZ这里使用了十六进制的整数表示方式。可以看到,movs指令将0x8FFF扩展以后存入%eax寄存器,其中%dx为寄存器%edx的后16位表示。

movz指令

movz指令的作用是将源操作数S做零扩展后,再复制到目的操作数中。它与movs指令十分相似,也有两个数据格式和两个操作数,因此一般的形式为[movzxy S D]。其中x、y为数据格式,S为源操作数,D为目的操作数。其中x、y的组合一共有三种,分别是bw、bl、wl,这三个组合代表的意思分别是单字节到双字节,单字节到双字以及双字节到双字。

这里依然采用相似的示例,我们来看看对于指令movzwl %dx %eax来讲,它的作用与上面的movs有何不同。
在这里插入图片描述

可以看出,movz与movs指令是十分相似的,只是这里扩展后,目标寄存器%eax的前16位为0而不再是1。

push指令

push指令与上面的mov族指令有着不同,它的目的操作数被固定为栈顶,因此它的指令当中没有目的操作数。另外有一点需要注意的是,它在进行复制操作之前,需要移动栈顶指针(-4)。push指令的一般形式为[pushl S],其中l代表数据格式为双字,S为源操作数,目的操作数默认为栈顶。

这里LZ举一个简单的例子,比如pushl %edx这条命令,它的任务是将%edx寄存器的值复制到栈顶。我们首先来看一下命令执行前,寄存器以及存储器的状态。

在这里插入图片描述
  可以看到,寄存器%ebp和%esp分别指向帧指针和栈指针,而%esp实际上就是指向的栈顶。由于现在栈顶位于-16的位置,因此若要将%edx压入栈,则先需要将栈顶移动到-20的位置,然后再进行复制,移动后的状态如下图所示。

在这里插入图片描述

可以看到,这里栈指针的位置已经发生了变化,向下移动了四位,并且将%edx寄存器的值放入新的栈顶,因此pushl %edx指令就相当于下面两条指令。

subl $4,%esp

movl %edx,(%esp)

这里可以看出来,其实pushl指令做了一个隐藏操作,就是移动栈指针(-4),这一点希望各位猿友们注意。

pop指令

pop指令与push指令是做的相反的操作,一个是入栈一个是出栈。对于pop指令来讲,它的源操作数被固定为栈顶,相反,它会先进行复制操作,然后再移动栈指针。pop指令的一般形式为[popl D],其中l代表数据格式为双字,D为目的操作数,源操作数默认为栈顶。

接下来我们举一个例子,与上面的例子类似,我们考虑popl %edx这条指令的效果,它会将栈顶的值弹出到寄存器%edx。首先来看执行之前,寄存器以及存储器的状态。

在这里插入图片描述
  接下来执行pop指令时,会先将栈顶的值复制到%edx,然后再将栈指针移动(+4)。我们来看一下它执行后的状态。

在这里插入图片描述

可以看到,之前栈顶的内容已经被弹出到%edx寄存器,并且当前栈顶已经移动到了-16的位置,也就是进行了+4操作。因此popl %edx指令就相当于下面两条指令。

movl (%esp),%edx

addl $4,%esp

这里可以看出来,其实popl指令也同样做了一个隐藏操作,就是移动栈指针(+4)。

数据复制示例

上面我们已经了解了几乎所有的数据复制指令,接下来我们写一小段程序,来看下这些数据复制指令,如何完成我们的程序操作。

simple(int *xp,int y){
    int t = *xp;
    *xp=y;
    return t;
}

上面是一个简单的C程序sum.c,它其中包含了一些赋值操作,我们来看看它的汇编代码。使用GCC -O1 -S sum.c来获取我们的汇编代码,并使用cat sum.s来查看一下。

   .file    "sum.c"
    .text
.globl simple
    .type    simple, @function
simple:
    pushl    %ebp
    movl    %esp, %ebp
    //以上为栈的建立部分
    movl    8(%ebp), %edx
    movl    (%edx), %eax
    movl    12(%ebp), %ecx
    movl    %ecx, (%edx)
    //以下为栈的完成部分
    popl    %ebp
    ret
    .size    simple, .-simple
    .ident    "GCC: (Ubuntu 4.4.3-4ubuntu5.1) 4.4.3"
    .section    .note.GNU-stack,"",@progbits

分析这段汇编代码的时候,我们应该分为三个部分来看待,首先是栈的建立、然后是使用、最后是完成部分。可以看到,里面几乎全是数据复制指令,我们先来看看栈的建立部分。

其实对于一开始pushl和movl指令来讲,它主要做了两件事。第一个是将原来的帧指针备份到栈顶,然后再将帧指针和栈指针统一指向这个新的栈顶,也就是完成了一个新栈的建立。它在完成后,栈的状态如下所示。

在这里插入图片描述

可以看到,寄存器%ebp和寄存器%esp都指向当前帧指针的位置,其中变量xp位于+8的位置,而y位于+12的位置。由于xp是一个指针变量,因此它会指向一个内存中的区域,其中的值为*xp。

了解完寄存器和存储器的状态,此时栈已经建立完毕,接下来我们看紧接着的一句汇编代码的作用。

movl    8(%ebp), %edx

这一句将内存地址为%ebp+8的值复制到%edx,很明显,从上面的图中可以看出,%ebp+8这个位置存储着xp变量。这一句指令做了一个简单的操作,就是将xp提取到%edx寄存器,如下所示。

在这里插入图片描述

此时已经将%edx的值改为了变量xp,看接下来的一句操作。

movl (%edx), %eax
  这一句将内存地址为%edx的值赋给寄存器%eax,并准备返回值。此时%edx寄存器的值已经改为了xp变量,因此(%edx)其实就是*xp,而%eax寄存器一般会作为函数的返回值,因此它其实替代了临时变量t。执行后的状态如下所示。

在这里插入图片描述

此时其实已经完成了程序中的int t = *xp以及为return t准备好了返回值,接下来的一句汇编代码作用也非常简单,如下。

movl    12(%ebp), %ecx

它的作用是将地址为%ebp+12的值复制到寄存器%ecx,从图中可以看出,%ebp+12就是存储的变量y。因此它的作用就是将y复制到寄存器%ecx,如下所示。
在这里插入图片描述

上面这一步挺简单,我们来看最后一步操作,如下。

movl    %ecx, (%edx)

它的作用是将%ecx寄存器的值复制到内存中%edx的位置。此时%ecx的值为y,而%edx中为xp,因此目的操作数则为xp指向的位置,也就是*xp。这一句话执行的就是程序代码当中,*xp=y这个操作,它执行后的状态如下所示。

在这里插入图片描述

可以看到,在执行了*xp=y以后,xp指针所指向的位置,其值已经变为了y。此时程序其实已经基本运行完毕,剩下的工作也就是栈的完成操作了,也就是popl指令。在栈完成之后,也就是pop指令执行之后,当前帧会恢复到调用者的帧上面去,如下所示。

在这里插入图片描述

此时当前帧已经恢复到了调用者的帧,最后ret指令会改变程序计数器(PC)的值,然后跳出子函数,继续执行调用者当中的代码。到此,我们的数据复制示例就结束了,尽管这个例子并不难,但是麻雀虽小五脏俱全,如果理解了这个过程,相信就算是再复杂一些的汇编指令,也只是分析的时间长一点罢了。

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值