汇编指令与高级编程语言一样,汇编语言也是一门完整的计算机编程语言,其涉及到的内容和知识点有很多,本文会介绍汇编语言下的子集-汇编指令,旨在让各位看完本文之后有能力读懂goroutine调度器中的汇编代码。
聊汇编指令之前,先聊一下机器指令。
二进制的机器指令才是CPU能够读懂的机器语言,因其是二进制格式的,非常便于CPU解析和执行,但对编程人员来说不是很友好,所以才有与机器指令相对应的汇编指令,它使用符号来表示机器指令。
看个例子感受下:
0x40054d : add %rdx,%rax // 汇编指令
(gdb) x/3xb 0x40054d
0x40054d : 0x48 0x01 0xd0 // 机器指令
(gdb)
同样是把rax和rdx寄存器中的值相加操作,汇编指令是add %rdx,%rax,机器指令确实三个数字【0x48 0x01 0xd0】。很明显,汇编指令对编程人员来说更友好,因其更容易进行记忆和读写操作。
不同CPU支持的机器指令不一样,所以其汇编指令也不同,即使是相同CPU,不同汇编工具和平台所使用的汇编指令格式也不尽相同。本文聊的是ADM64 Linux下Go调度器所支持的AT&T格式的汇编指令,看下基本格式:
操作码 [操作数]
说明如下:
-
操作码。表示CPU执行加减法还是读写内存操作,每条指令必须有操作码。
-
操作数。表示CPU操作的对象,如加法操作的两个加数就是这条指令的操作数,一般有0、1、2个值。
接下来看几个汇编指令的例子:
add %rdx,%rax
上述指令操作码是add,表示执行加法操作,它的两个操作数分别为rdx和rax。
如果一条指令有两个操作数,那第一个操作数就叫做源操作数,第二个就叫做目的操作数,顾名思义,目的操作数表示的就是这条指令执行完毕之后的结果应该保存的地方。
上述指令完整含义是对寄存器rdx和rax的值求和,之后把结果值存放在rax中。
其实上述指令第二个操作数寄存器rax既是源操作数,也是目的操作数,因为rax也是加法操作的两个加数之一,还得存放结果值,所以在指令执行完之后,rax的值发生变化,指令执行前rax的值因被覆盖而丢失,如果rax的值还有用,就得先用指令在求和操作之前将rax中的值存到别的寄存器中。
再看个只有一个操作数的例子:
callq 0x400526
上述指令操作码是callq,表示调用函数,操作数是0x400526,这是函数地址。
最后看个没有操作数的指令:
retq
上述指令操作码是retq,表示从被调用函数返回到调用函数继续执行。
AT%T格式的汇编指令的总结说明如下:
-
寄存器名称需加%作为前缀。
-
有两个操作数的指令中,第一个是源操作数,第二个是目的操作数。
-
立即操作数。操作数需加$作为前缀,如【mov $0x1 %rdi】,表示把数值0x1放到寄存器rdi,这条指令第一个操作数不是寄存器,也不是内存地址,而是直接写在指令中的一个常数,这种操作数叫做立即操作数。
-
寄存器间接寻址格式为 offset(%register) ,如offset为0,则可略去偏移直接写成(%register)。
-
与内存相关部分指令操作码会加上b、w、l、q字母分别表示操作的内存是1、2、4、8个字节,如movl $0x0,-0x8(%rbp)的操作码movl后缀字母l表示把从-0x8(%rbp)这地址开始的4个内存单元赋值为0。
可能有人会问了,如果需要操作3、5、7个字节怎么办?
CPU没有提供相应的单条指令,只能通过多条指令组合使用来达到目的。
寄存器间接寻址意思就是指令中的寄存器的值是一个内存地址,而不是真正的源操作数或目的操作数,这个地址对应的值才是真正的源操作数或目的操作数。如 mov %rax,(%rsp) 指令,其中第二个操作数被括号括住表示间接寻址,rsp是一个内存地址,所以上述指令的真正含义就是把rax中的值赋值给rsp中的内存地址对应的内存,rsp本身的值不会被修改,对比来看下 mov %rax,%rsp ,与上述指令相比少了括号,但这就变成了直接寻址,它的含义就是把rax的值赋值给rsp,rsp的值在这时候就被修改了。
通过图例理解下直接寻址方式:
由上图可知,指令执行前,rsp值是x,rax是y,执行后,rax的值赋值给rsp,所以rsp变成了y,可以看到,采用直接寻址方式时,目的操作数rsp在指令执行前后发生变化,源操作数没发生变化。
通过图例理解下间接寻址方式:
由上图可以,指令执行前,rax的值是y,rsp是x,这个x是个内存地址,上图中用红色箭头从rsp指向了地址为x的内存,指令执行后,rsp的值没有变化,但地址为x的内存却因为指令采用间接寻址的方式而发生了变化,执行的结果是rax的值被复制到rsp的值所指向的8个内存单元。
需注意的是,指令中出现的内存地址仅是起始地址,具体如何操作要以此地址为起点,至于操作连续内存单元的数量则要根据具体的指令而定,如上述图中指令的源操作数是一个64位寄存器,所以此指令会复制rax存储的8个字节到地址为x、x+1、x+2、x+3、x+4、x+5、x+6、x+7这8个内存单元中。
间接寻址格式offset(%register)中,offset表示偏移量,如-0x8(%rbp)中的-0x8就是偏移量,表示整个rbp存储的内存地址减去8(因偏移量是-8)得到的地址对应的内存。
来看一些常用指令详解。
1、mov指令。
mov 源操作数 目的操作数
上述指令将源操作数复制到目的操作数。看个案例:
mov %rsp,%rbp # 直接寻址,把rsp的值拷贝给rbp,相当于 rbp = rsp
mov -0x8(%rbp),%edx # 源操作数间接寻址,目的操作数直接寻址。从内存中读取4个字节到edx寄存器
mov %rsi,-0x8(%rbp) # 源操作数直接寻址,目的操作数间接寻址。把rsi寄存器中的8字节值写入内存
2、add、sub指令。
add 源操作数 目的操作数
sub 源操作数 目的操作数
上述指令为加减运算指令。看个案例:
sub $0x350,%rsp # 源操作数是立即操作数,目的操作数直接寻址。rsp = rsp - 0x350
add %rdx,%rax # 直接寻址。rax = rax + rdx
addl $0x1,-0x8(%rbp) # 源操作数是立即操作数,目的操作数间接寻址。内存中的值加1(addl后缀字母l表示操作内存中的4个字节)
3、call、ret指令。
call 目标地址
ret
call指令执行函数调用。CPU执行call指令时,会先把rip寄存器中的值入栈,然后设置rip值为目标地址,又因为rip决定了下一条需要执行的指令,所以当CPU执行完当前call指令之后就会跳转到目标地址去执行。
ret指令从被调用函数返回调用函数,实现原理是把call指令入栈的返回地址弹出给rip。
来看案例:
#调用函数片段
0x0000000000400559 : callq 0x400526 <sum>
0x000000000040055e : mov %eax,-0x4(%rbp)
#被调用函数片段
0x0000000000400526 : push %rbp
......
0x000000000040053f : retq
上述案例中,调用函数使用callq 0x400526指令调用0x400526处的函数,0x400526是被调用函数的第一条指令所在的地址,被调用函数在0x40053f处执行retq指令返回调用函数继续执行0x40055e地址处的指令。
需要注意的是,这两条指令会涉及出入栈操作,会影响rsp的值。
来看图例感受下:
由上图可知,call指令执行之初,寄存器rip的值是紧跟call后面那一条指令的地址,即0x40055e,但当call完成后又未开始执行下一条指令前,rip的值变成call指令的操作数,即被调用函数地址0x400526,这样CPU就会跳到被调用函数去执行。
这里call执行时将其后面那条指令地址0x40055e给push到栈上,所以这条call指令修改了三个地方:rip,rsp以及栈。
再来看下从被调用函数返回调用函数时执行的ret指令的图例:
由上图可知,ret执行操作与call完全相反,ret开始执行时,rip的值是紧跟ret指令后面那条,也就是0x400540,但ret执行过程中会把之前的call给push到栈中,0x40055e 给pop到rip,当ret执行完后会从被调用函数返回到调用函数的call指令的下一条指令继续执行,同时也要清楚,retq也会修改rsp的值。
4、jmp/je/jle/jg/jge等以j开头的指令。
属于跳转指令,操作码后面直接跟要跳转的地址或是存有跳转地址的寄存器。这些指令与高级编程语言中的goto和if等语句对应。
案例如下:
jmp 0x4005f2
jle 0x4005ee
jl 0x4005b8
5、push、pop指令。
push 源操作数
pop 目的操作数
专用于函数调用栈的出入栈操作的指令,都会修改寄存器rsp的值。
push入栈时rsp的值会先减去8把栈位置留出来,然后把操作数复制到rsp所指位置。
push相当于:
sub $8,%rsp
mov 源操作数,(%rsp)
来看个图例感受下:
push要重点关注rsp的变化。
pop出栈时先把寄存器rsp的值所指的内存地址对应的数据复制到目的操作数之中,之后寄存器rsp的值加8。
pop指令相当于:
mov (%rsp),目的操作数
add $8,%rsp
来看个图例感受下:
pop指令也需要重点关注rsp的变化。
6、leave指令。
如果没有操作数,一般放在函数尾部以及ret指令之前,用于调整rsp和rbp,相当于如下指令:
mov %rbp,%rsp
pop %rbp
本文到这里就结束了,如果喜欢的话,就来个三连击吧。
扫码关注公众号,获取更多优质内容。