CSAPP–第三章-程序的机器级表示(中)

Aki 发布于 2022-10-26 282 次阅读


【CSAPP-深入理解计算机系统】3-2.寄存器与数据传送指令_哔哩哔哩_bilibili

立即数寻址、寄存器寻址、直接寻址、间接寻址 - 知乎 (zhihu.com)

寄存器指令、

寄存器指令,大多数指令包含两部分:操作码和操作数。大多数指令具有一个或多个操作数,ret返回指令则没有操作数。在AT&T格式汇编中,立即数以$符号开头,后跟一个C语言定义的整数。操作数是寄存器的情况,即使在64位的处理器上,不仅64位的寄存器可以作为操作数,32位、16位甚至8位的寄存器都可以作为操作数。寄存器带小括号表示内存引用。我们通常将内存抽象成一个字节数组,当需要从内存中存取数据时,需要获得目的数据的起始地址addr,以及数据长度b。为了简便,通常会省略下标b。

最常用的内存引用包含四部分,一个立即数,一个基址寄存器,一个变址寄存器,一个比例因子。有效地址是通过立即数与基址寄存器的值相加,再加上变址寄存器与比例因子的乘积,如下所示。

需要注意,比例因子s取值必须是1、2、4、8。实际上比例因子的取值是与源代码中定义的数组类型的是相关的,编译器会根据数组的类型来确定比例因子的数值,例如:定义char类型的数组,比例因子就是1int类型,比例因子就是4,至于double类型比例因子就是8,其他形式的内存引用都是这些普通形式的变种,省略了某些部分。

mov指令、

mov含有两个操作数,一个是源操作数,另一个是目的操作数。源操作数可以是立即数、寄存器、内存引用等。目的操作数是用来存放源操作数内容的,因此目的操作数可为寄存器或内存引用等,目的操作数不能是立即数。我们在mov指令后经常看到 movq、movw、movb等的形式,代表移动的位数,指令mov的后缀一定要和寄存器的大小进行匹配,如果是32位寄存器,就需要movl;如果是64位寄存器,就需movq,如果是8位,则movb,如果是16位则movw。

需要注意,x86_64处理器有一条限制,mov指令的源操作数和目的操作数不能都是内存的地址,当需要将一个数从内存的一个位置复制到另一个位 置时,需要两条mov指令来完成;第一条指令将内存源位置的数值加载到寄存器;第二条指令再将该寄存器的值写入内存的目的位置,如下所示。

mov  sour_memory, register
mov  register, des_memory

mov指令源操作数是立即数Imm时,立即数只能32位补码,对该32位源操作数进行符号位扩展,传送至64位目的位置(可以是寄存器也可以是内存)。整数的补码,原码,反码相同,负数在计算机上以补码表示。

如果要将64位补码移动至寄存器(而不能是内存)中,可以使用movabsq指令,该指令只能将64位补码移动至寄存器中而非内存,这里需要注意。

mov指令如何修改目的寄存器的内容

movabsq $0x0011223344556677, %rax

指的是将立即数存放至寄存器中,如下图所示:

现在要将一个只有8位的立即数-1复制到寄存器al当中,那么首先什么是al寄存器?其实在寄存器发展当中,随着寄存器位数的增加,很多寄存器的低位依然保留原来的名字,高位衍生出新的名字具体如下:

所以al寄存器实际上就是rax寄存器的低8位,那么回到刚才,将8位立即数-1复制到寄存器al中,使用movb指令:movb $-1, %al,寄存器低8位发生改变,那么为什么是全F呢,因为-1的补码是全F即0xff,寄存器存放的是立即数的补码:

那如果要将低16位的立即数-1即0xffff复制到寄存器中,首先得复制ax寄存器,ax寄存器是rax寄存器的低16位,其次要用到命令movw,即movw $-1, %ax

使用指令movl $-1 %eax,此时寄存器的低32位即4字节都变为f,而且高32位也要发生变化,当movl 的目的操作数是寄存器时,它会把高32位全设置为0,这是64位处理器的一个规定。即任何位寄存器生成32位值的指令,都会把该寄存器的高位设置为0。

以上介绍了mov指令的位扩展等操作,但是都基于一个前提那就是源操作数与目的操作数的数位相同。

当源操作数位小于目的操作数的数位时,需要对目的操作数剩余字节进行零扩展或符号位扩展,具体是哪种扩展,与指令相关。零扩展数据传送指令有5条,指令后z是zero的缩写;符号位扩展传送指令有6条,指令后s是sign的缩写。接下来的第一个字母是源操作数大小,第二个字母表示目的操作数的大小,指令如图所示:

可以看到符号扩展比零扩展多一条4字节到8字节的扩展指令movslq,为何零扩展无movzlq?是因为movl指令可实现该扩展,即我们上面提到的,如movl $-1, %eax,当低32位使用F填充后,高32位必须置0,即movl实现了类似于movzlq的功能,所以无需指令movzlq。同时需要说明符号位扩展中的cltq指令,该指令的源操作数总是寄存器eax,目的操作数总是寄存器rax,cltq的效果等价于执行了movslq %eax, %rax,即将eax高32位用符号位扩展。

数据传输指令、

对一个执行的程序而言,若要计算加法c = a + b c = a+bc=a+b,那么需要将数据通过内存总线和系统总线从内存中写入寄存器中,然后通过CPU内部的逻辑运算单元ALU来计算a和b的加法,将返回值写给rax、eax、ax、al等相关位数寄存器(具体使用哪个和数据类型字宽有关),需要再次说明,之所以ALU的结果放到rax中,因为rax这个特定寄存器的功能就是用来存放返回值的。

举个例子,我们来看下如下代码,分析其汇编执行流程:

我们使用gcc -Og -S exchange.c 单独对exchange函数进行汇编,生成汇编代码主要指令如下:

exchange:
   movq  (%rdi), %rax
   movq  %rsi, (%rdi)
   ret

根据之前的学习,我们知道第一个参数存放的位置在rdi中,第二个参数存放的位置在rsi中(均为long 8字节类型)。所以xp指针指向的值的地址保存在rdi中,y的值存放在rsi中,函数exchange主要有三条指令实现,包括两条数据传送指令和一条返回指令。

此外,还有两个数据传送指令需要借助程序栈,程序栈本质上是内存中的一个区域。栈的增长方向是从高地址向低地址,因此,栈顶的元素是所有栈中元素地址中最低的。根据惯例,栈是倒过来画的,栈顶在图的底部,栈底在顶部,rsp是栈顶寄存器。

例如现在我们需要保存寄存器rax内存储的数据0x123,可以使用pushq指令把数据压入栈内。若要将数据弹出,则使用popq指令,这些指令只有一个操作数(压入的数据源和弹出的数据目的)。

我们首先来看下一个入栈的操作过程:

首先指向栈顶的寄存器的rsp进行一个减法操作,例如压栈之前,栈顶指针rsp指向栈顶的位置,此处的内存地址0x108;压栈的第一步就是寄存器rsp的值减8,此时指向的内存地址是0x100

然后将需要保存的数据复制到新的栈顶地址,此时,内存地址0x100处将保存寄存器rax内存储的数据0x123。实际上pushq的指令等效于subq和movq这两条指令。它们之间的区别是在于pushq这一条指令只需要一个字节,而subq和movq这两条指令需要8个字节。所以执行pushq %rax意味着执行了两个操作,首先是将栈顶地址减8,然后再将rax寄存器存放的值存放至栈顶指针rsp指向的位置。

说到底,push指令的本质还是将数据写入到内存中,那么与之对应的pop指令就是从内存中读取数据,并且修改栈顶指针。例如图中这条popq指令就是将栈顶保存的数据复制到寄存器rbx中。那么pop操作也可分解为两部分:

首先从栈顶的位置读出数据,复制到寄存器rbx(被调用者保存寄存器)。此时,栈顶指针rsp指向的内存地址是0x100

然后将栈顶指针加8pop后栈顶指针rsp指向的内存地址是0x108

因此pop操作也可以等效movqaddq这两条指令。实际上pop指令是通过修改栈顶指针所指向的内存地址来实现数据删除的,此时,内存地址0x100内所保存的数据0x123仍然存在,直到下次push操作,此处保存的数值才会被覆盖。