【CSAPP-深入理解计算机系统】3-2.寄存器与数据传送指令_哔哩哔哩_bilibili
立即数寻址、寄存器寻址、直接寻址、间接寻址 - 知乎 (zhihu.com)
寄存器指令、
寄存器指令,大多数指令包含两部分:操作码和操作数。大多数指令具有一个或多个操作数,ret返回指令则没有操作数。在AT&T格式汇编中,立即数以$符号开头,后跟一个C语言定义的整数。操作数是寄存器的情况,即使在64位的处理器上,不仅64位的寄存器可以作为操作数,32位、16位甚至8位的寄存器都可以作为操作数。寄存器带小括号表示内存引用。我们通常将内存抽象成一个字节数组,当需要从内存中存取数据时,需要获得目的数据的起始地址addr,以及数据长度b。为了简便,通常会省略下标b。

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

需要注意,比例因子s
取值必须是1、2、4、8。实际上比例因子的取值是与源代码中定义的数组类型的是相关的,编译器会根据数组的类型来确定比例因子的数值,例如:定义char
类型的数组,比例因子就是1
,int
类型,比例因子就是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
。

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

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

Comments NOTHING