本文是《go调度器源代码情景分析》系列 第一章 预备知识的第4小节。

汇编语言是每位后端程序员都应该掌握的一门语言,因为学会了汇编语言,不管是对我们调试程序还是研究与理解计算机底层的一些运行原理都具有非常重要的作用,所以建议有兴趣的读者可以多花点时间把它学好。

与高级编程语言一样,汇编语言也是一门完整的计算机编程语言,它所涉及的知识内容也很多,好在我们的主要目标是通过对本小节的学习而有能力去读懂汇编代码,而不是要用汇编语言去写代码,所以本节并不会全面介绍汇编语言,而只会选取汇编语言的一个子集--汇编指令出来做介绍。不过,虽然这里的介绍做了精简,但读者大可放心,熟练运用这些知识就足以应付本书将要分析的goroutine调度器中的汇编代码了。

说到汇编指令,不得不提一下机器指令,二进制格式的机器指令才是CPU能够理解的语言,因为它是二进制格式的,非常便于CPU的解析和执行,但并不利于人类阅读和交流,所以才有了跟机器指令一一对应的汇编指令,汇编指令使用符号来表示机器指令,下面的例子非常直观的说明了这两种指令之间的差异:

0x40054d: add   %rdx,%rax  # 汇编指令

(gdb) x/3xb 0x40054d
0x40054d: 0x48 0x01 0xd0 # 机器指令
(gdb)

同样是把rdx和rax寄存器中的值相加,汇编指令为:add    %rdx,%rax,而机器指令却是三个数字:0x48 0x01 0xd0,显然,汇编指令对人类来说更加友好,它更加易记易读和易写。

汇编指令格式

因为不同的CPU所支持的机器指令不一样,所以其汇编指令也不同,即使是相同的CPU,不同的汇编工具和平台所使用的汇编指令格式也有些差别,由于本书主要专注于AMD64 Linux平台下的go调度器,因此下面我们只介绍该平台下所使用的AT&T格式的汇编指令,AT&T汇编指令的基本格式为:

操作码  [操作数]

可以看到每一条汇编指令通常都由两部分组成:

  • 操作码:操作码指示CPU执行什么操作,比如是执行加法,减法还是读写内存。每条指令都必须要有操作码。

  • 操作数:操作数是操作的对象,比如加法操作需要两个加数,这两个加数就是这条指令的操作数。操作数的个数一般是0个,1个或2个。

来看几个汇编指令的例子

add  %rdx,%rax

这条指令的操作码是add,表示执行加法操作,它有两个操作数,rdx和rax。如果一条指令有两个操作数,那么第一个操作数叫做源操作数,第二个操作数叫做目的操作数,顾名思义,目的操作数表示这条指令执行完后结果应该保存的地方。所以上面这条指令表示对rax和rdx寄存器里面的值求和,并把结果保存在rax寄存器中。其实这条指令的第二个操作数rax寄存器既是源操作数也是目的操作数,因为rax既是加法操作的两个加数之一,又得存放加法操作的结果。这条指令执行完后rax寄存器的值发生了改变,指令执行前的值被覆盖而丢失了,如果rax寄存器之前的值还有用,那么就得先用指令把它保存到其它寄存器或内存之中。

再来看一个只有一个操作数的例子:

callq 0x400526 

这条指令的操作码是callq,表示调用函数,操作数是0x400526,它是被调用函数的地址。

最后来看一条没有操作数的指令:

retq

这条指令只有操作码retq,表示从被调用函数返回到调用函数继续执行。

为了更好的理解AT&T格式的汇编指令,这里先对其格式做一个简要的说明:

  • AT&T格式的汇编指令中,寄存器名需要加%作为前缀,前面我们已经见过;
  • 有2个操作数的指令中,第一个操作数是源操作数,第二个是目的操作数,刚才也讨论过,不过那条指令中的源和目的不是那么清晰,来看一个直白的,mov    %eax,%esi,这条指令表示把eax寄存器中的值拷贝给esi,这条指令中源和目的就很清楚了;
  • 立即操作数需要加上$符号做前缀,如  "mov $0x1 %rdi" 这条指令中第一个操作数不是寄存器,也不是内存地址,而是直接写在指令中的一个常数,这种操作数叫做立即操作数。这条指令表示把数值0x1放入rdi寄存器中。
  • 寄存器间接寻址的格式为  offset(%register),如果offset为0,则可以略去偏移不写直接写成(%register)。何为间接寻址呢?其实就是指指令中的寄存器并不是真正的源操作数或目的操作数,寄存器的值是一个内存地址,这个地址对应的内存才是真正的源或目的操作数,比如 mov    %rax, (%rsp)这条指令,第二个操作数(%rsp)中的寄存器的名字用括号括起来了,表示间接寻址,rsp的值是一个内存地址,这条指令的真实意图是把rax寄存器中的值赋值给rsp寄存器的值(内存地址)对应的内存,rsp寄存器本身的值不会被修改,作为比较,我们看一下 mov    %rax, %rsp 这条指令 ,这里第二个操作数仅仅少了个括号,变成了直接寻址,意思完全不一样了,这条指令的意思是把rax的值赋给rsp,这样rsp寄存器的值被修改为跟rax寄存器一样的值了。下面的2张图展示了这两种寻址方式的不同:

执行mov %rax, %rsp这条指令之前,rsp寄存器的值是x,rax寄存器的值是y,执行指令之后,rax寄存器的值被复制给了rsp寄存器,所以rsp寄存器的值变成了y,可以看出,采用直接寻址方式时,目的操作数rsp寄存器的值在指令执行之前和指令执行之后发生了变化,源操作数没有变化。再看看间接寻址方式的示意图:

执行mov %rax, (%rsp)这条指令之前,rax寄存器的值是y,rsp寄存器的值是X,它是一个内存地址,如上图所示,我们用了一个红色箭头从rsp寄存器指向了地址为X的内存;执行指令之后,rsp寄存器的值并没有发生变化,而rsp所指的内存中的值却发生了改变,因为这条指令的目的操作数采用了间接寻址方式(%rsp),指令执行的结果是rax寄存器中的值被复制到了rsp寄存器存放的地址所对应的8个内存单元中。另外需要注意的是指令中出现的内存地址仅仅是起始地址,具体要操作以这个地址为起始地址的连续几个内存单元要根据具体的指令而定,比如上图中的mov %rax,(%rsp),因为源操作数是一个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)得到的地址对应的内存。

 

  • 与内存相关的一些指令的操作码会加上b, w, l和q字母分别表示操作的内存是1,2,4还是8个字节,比如指令 movl   $0x0,-0x8(%rbp) ,这条指令操作码movl的后缀字母l说明我们要把从-0x8(%rbp) 这个地址开始的4个内存单元赋值为0。可能有读者会问,那如果我要操作3个,或5个内存单元呢?很遗憾的是cpu没有提供相应的单条指令,我们只能通过多条指令组合起来达到目的。

常用指令详解

x86-64汇编指令上千条,这里不会去详细讲解每一条,读者如果有兴趣可以参考汇编语言相关教程。我们在这里着重关注几条非常常见或是能帮助我们理解程序运行机制的指令。

  • mov指令

mov  源操作数 目的操作数

该指令复制源操作数到目的操作数。例:

mov%rsp,%rbp      # 直接寻址,把rsp的值拷贝给rbp,相当于 rbp = rsp
mov-0x8(%rbp),%edx# 源操作数间接寻址,目的操作数直接寻址。从内存中读取4个字节到edx寄存器
mov%rsi,-0x8(%rbp) # 源操作数直接寻址,目的操作数间接寻址。把rsi寄存器中的8字节值写入内存
  • add/sub指令

add  源操作数 目的操作数
sub 源操作数 目的操作数

加减运算指令。例:

sub$0x350,%rsp      # 源操作数是立即操作数,目的操作数直接寻址。rsp = rsp - 0x350
add%rdx,%rax # 直接寻址。rax = rax + rdx
addl$0x1,-0x8(%rbp) # 源操作数是立即操作数,目的操作数间接寻址。内存中的值加1(addl后缀字母l表示操作内存中的4个字节)
  • 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指令执行时把call指令后面那一条指令的地址 0x40055e PUSH到了栈上,所以一条call指令修改了3个地方的值:rip寄存器、rsp和栈

下面我们再看看从被调用函数返回调用函数时执行的ret指令,其示意图如下:

可以看到ret指令执行的操作跟call指令执行的操作完全相反,ret指令开始执行时rip寄存器的值是紧跟ret指令后面的那个地址,也就是0x400540,但ret指令执行过程中会把之前call指令PUSH到栈上的返回地址 0x40055e POP给rip寄存器,这样,当ret执行完成后就会从被调用函数返回到调用函数的call指令的下一条指令继续执行。这里同样要注意的是retq指令也会修改rsp寄存器的值。

  • jmp/je/jle/jg/jge等等j开头的指令

这些都属于跳转指令,操作码后面直接跟要跳转到的地址或存有地址的寄存器,这些指令与高级编程语言中的 goto 和 if 等语句对应。用法示例:

jmp   0x4005f2
jle 0x4005ee
jl 0x4005b8
  • push/pop指令

push  源操作数
pop 目的操作数

专用于函数调用栈的入栈出栈指令,这两个指令都会自动修改rsp寄存器

push入栈时rsp寄存器的值先减去8把栈位置留出来,然后把操作数复制到rsp所指位置。push指令相当于:

sub   $,%rsp
mov 源操作数,(%rsp)

push指令需要重点注意rsp寄存器的变化。

pop出栈时先把rsp寄存器所指位置的数据复制到目的操作数中,然后rsp寄存器的值加8。pop指令相当于:

mov(%rsp),目的操作数
add$,%rsp

同样,pop指令也需要重点注意rsp寄存器的变化。

  • leave指令

leave指令没有操作数,它一般放在函数的尾部ret指令之前,用于调整rsp和rbp,这条指令相当于如下两条指令:

mov%rbp,%rsp
pop%rbp

AMD64汇编我们就介绍这么多,下一节我们将介绍goruntime中使用的go汇编语言,它与这里介绍的AMD64汇编类似,但有一些差别。理解了本节的内容,go汇编也就很容易理解了。

go语言调度器源代码情景分析之五:汇编指令的更多相关文章

  1. go语言调度器源代码情景分析之四:函数调用栈

    本文是<go调度器源代码情景分析>系列 第一章 预备知识的第3小节. 什么是栈 栈是一种“后进先出”的数据结构,它相当于一个容器,当需要往容器里面添加元素时只能放在最上面的一个元素之上,需 ...

  2. go语言调度器源代码情景分析之三:内存

    本文是<go调度器源代码情景分析>系列 第一章 预备知识的第2小节. 内存是计算机系统的存储设备,其主要作用是协助CPU在执行程序时存储数据和指令. 内存由大量内存单元组成,内存单元大小为 ...

  3. go语言调度器源代码情景分析之二:CPU寄存器

    本文是<go调度器源代码情景分析>系列 第一章 预备知识的第1小节. 寄存器是CPU内部的存储单元,用于存放从内存读取而来的数据(包括指令)和CPU运算的中间结果,之所以要使用寄存器来临时 ...

  4. go语言调度器源代码情景分析之一:开篇语

    专题简介 本专题以精心设计的情景为线索,结合go语言最新1.12版源代码深入细致的分析了goroutine调度器实现原理. 适宜读者 go语言开发人员 对线程调度器工作原理感兴趣的工程师 对计算机底层 ...

  5. go语言调度器源代码情景分析之六:go汇编语言

    go语言runtime(包括调度器)源代码中有部分代码是用汇编语言编写的,不过这些汇编代码并非针对特定体系结构的汇编代码,而是go语言引入的一种伪汇编,它同样也需要经过汇编器转换成机器指令才能被CPU ...

  6. 《Android系统源代码情景分析》连载回忆录:灵感之源

    上个月,在花了一年半时间之后,写了55篇文章,分析完成了Chromium在Android上的实现,以及Android基于Chromium实现的WebView.学到了很多东西,不过也挺累的,平均不到两个 ...

  7. linux调度器源码分析 - 初始化(二)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言 上期文章linux调度器源码分析 - 概述(一)已经把调度器相关的数据结构介绍了一遍,本篇着重通过代码说明 ...

  8. [翻译] 深入浅出Go语言调度器:第一部分 - 系统调度器

    目录 译者序 序 介绍 系统调度器 执行指令 Figure 1 Listing 1 Listing 2 Listing 3 线程状态 任务侧重 上下文切换 少即是多 寻找平衡 缓存行 Figure 2 ...

  9. Linux内核源代码情景分析系列

    http://blog.sina.com.cn/s/blog_6b94d5680101vfqv.html Linux内核源代码情景分析---第五章 文件系统  5.1 概述 构成一个操作系统最重要的就 ...

随机推荐

  1. angular 表单验证

    最近在用angular写表单验证时 , 不小心把ng-model全替换删掉了, 然后发现之前写的验证都失效, 在查阅资料和反复修改摸索后, 发现angular中的表单验证, 都是基于ng-model的 ...

  2. python_code list_1

    >>> def is_not_empty(s): return s and len(s.strip()) > 0 >>> filter(is_not_empt ...

  3. SpringCloud实战-Hystrix线程隔离&请求缓存&请求合并

    接着上一篇的Hystrix进行进一步了解. 当系统用户不断增长时,每个微服务需要承受的并发压力也越来越大,在分布式环境中,通常压力来自对依赖服务的调用,因为亲戚依赖服务的资源需要通过通信来实现,这样的 ...

  4. springMVC(spring)+WebSocket案例(获取请求参数)

    开发环境(最低版本):spring 4.0+java7+tomcat7.0.47+sockjs 前端页面要引入: <script src="http://cdn.jsdelivr.ne ...

  5. SSH整合配置文件概括

    配置方式一:struts.xml, applicationContext.xml(hibernate.cfg.xml配置信息写入spring配置文件中) (版本号, struts2:2.3.15; s ...

  6. LoadRunner性能测试工具

    1:LoadRunner是一款性能测试软件,通过模拟真实的用户行为,通过负载.并发和性能实时监控以及完成后的测试报告,分析系统可能存在的瓶颈,LoadRunner最为有效的手段之一应该就是并发的控制. ...

  7. 解决redis connection refused: connect无法连接redis

    环境 VM VirtualBox安装虚拟机CentOS 7 1.Redis.conf配置文件中 注释掉 bind 127.0.0.1 2.防火墙关闭(或添加可访问的端口,具体不在此描述) 最后一个坑 ...

  8. Oracle-09:聚合函数

    ------------吾亦无他,唯手熟尔,谦卑若愚,好学若饥------------- 数据库脚本放一下,供测试使用 create table DEPT ( deptno ) not null, d ...

  9. Jmeter4.0分布式测试时启动Jmeter.server时报错

       最近又开始研究Jmeter,将新版本4.0下载下来体验,准备远程分布式测试,又出现一些问题,废话不多说,直入主题把! Windows 系统启动Jmeter 4.0的JmeterServer.ba ...

  10. JavaWeb(一)JavaWeb应用的概念

    JavaWeb应用的概念 在Sun的Java Servlet规范中,对Java Web应用作了这样定义:"Java Web应用由一组Servlet.HTML页.类.以及其它可以被绑定的资源构 ...