原创文章,欢迎转载,转载请注明出处,谢谢。


0. 前言

作为一个严肃的 Gopher,了解汇编是必须的。本汇编系列文章会围绕基本的 Go 程序介绍汇编的基础知识。

1. Go 程序到汇编

首先看一个简单到令人发指的示例:

package main

func main() {
a := 1
print(a)
}

运行程序,输出:

# go run ex0.go
1

当使用 go run 运行程序时,代码会经过编译,链接,执行得到输出,这是自动执行的,没办法查看中间过程。我们可以使用 dlv 查看这段代码在执行时做了什么。dlv 将代码加载到内存中交给 CPU 执行,又不丧失对 CPU 的控制。换言之,我们是在底层通过 dlv 对 CPU 进行调试查看代码的执行过程,这对我们了解程序的执行是非常有帮助的。

使用 dlv debug 调试程序:

# go mod init ex0
go: creating new go.mod: module ex0
go: to add module requirements and sums:
go mod tidy # dlv debug
Type 'help' for list of commands.
(dlv)

使用 disass 可查看应用程序的汇编代码,这里的汇编是真实的机器执行的汇编代码。汇编是离机器最近的“语言”,翻译成汇编可以帮助我们知道机器在对我们的代码做什么。

(dlv) disass
TEXT _rt0_amd64_linux(SB) /usr/local/go/src/runtime/rt0_linux_amd64.s
=> rt0_linux_amd64.s:8 0x466d00 e95bc9ffff jmp $_rt0_amd64

从这段汇编代码可以看出,进入 main 函数前,机器执行的是 Go runtime 中 rt0_linux_amd64.s 第 8 行的汇编指令。查看 rt0_linux_amd64.s

// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. #include "textflag.h" TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
JMP _rt0_amd64(SB) TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0
JMP _rt0_amd64_lib(SB)

第 8 行执行的是 JMP _rt0_amd64(SB) 跳转指令。

使用 si 命令单步调试,si 是指令级调试。执行 si 查看的是 CPU 执行的下一条指令:

(dlv) si
> _rt0_amd64() /usr/local/go/src/runtime/asm_amd64.s:16 (PC: 0x463660)
Warning: debugging optimized function
TEXT _rt0_amd64(SB) /usr/local/go/src/runtime/asm_amd64.s
=> asm_amd64.s:16 0x463660 488b3c24 mov rdi, qword ptr [rsp]
asm_amd64.s:17 0x463664 488d742408 lea rsi, ptr [rsp+0x8]
asm_amd64.s:18 0x463669 e912000000 jmp $runtime.rt0_go

CPU 执行的是 runtime/asm_amd64.s 中的汇编指令。查看 runtime/asm_amd64.s

// _rt0_amd64 is common startup code for most amd64 systems when using
// internal linking. This is the entry point for the program from the
// kernel for an ordinary -buildmode=exe program. The stack holds the
// number of arguments and the C-style argv.
TEXT _rt0_amd64(SB),NOSPLIT,$-8
MOVQ 0(SP), DI // argc
LEAQ 8(SP), SI // argv
JMP runtime·rt0_go(SB)

可以看到,Go runtime 的汇编和机器实际执行的汇编指令有所出入。这里 Go 的汇编可以理解成在汇编之上又定制的一层汇编,要注意的是机器实际执行的是 Go 汇编翻译之后的汇编。

1.1 main 函数栈

本文的重点并不是单步调试 runtime 的汇编指令,我们使用 b 给 main 函数加断点,使用 c 执行到断点处,重点看 main 函数中的执行过程:

(dlv) b main.main
Breakpoint 1 set at 0x45feca for main.main() ./ex0.go:3
(dlv) c
> main.main() ./ex0.go:3 (hits goroutine(1):1 total:1) (PC: 0x45feca)
1: package main
2:
=> 3: func main() {
4: a := 1
5: print(a)
6: }

程序执行到 ex0.go 的第三行。disass 查看汇编指令:

(dlv) disass
TEXT main.main(SB) /root/go/src/foundation/ex0/ex0.go
ex0.go:3 0x45fec0 493b6610 cmp rsp, qword ptr [r14+0x10]
ex0.go:3 0x45fec4 762b jbe 0x45fef1
ex0.go:3 0x45fec6 55 push rbp
ex0.go:3 0x45fec7 4889e5 mov rbp, rsp
=> ex0.go:3 0x45feca* 4883ec10 sub rsp, 0x10

汇编代码显示执行到内存地址 0x45feca 处,内存地址中存储的是汇编指令 sub rsp, 0x10,对应的十六进制是 4883ec10,转换为二进制机器指令是 1001000100000111110110000010000

我们有必要分段介绍执行 sub rsp, 0x10 前 CPU 执行的指令,以方便理解。

首先,cmp rsp, qword ptr [r14+0x10] 指令比较 rsp 寄存器的值和 [r14+0x10] 寄存器中的值,并将比较的结果存储到标志寄存器中。

接下来,指令 jbe 0x45fef1 将读取标志寄存器的结果,如果比较结果 rsp 小于或等于 [r14+0x10] 则跳转到内存 0x45fef1。查看 0x45fef1 中存储的指令:

ex0.go:3        0x45fef1        e8eacdffff              call $runtime.morestack_noctxt

0x45fef1 存储的是 runtime.morestack_noctxt 函数的调用。

机器指令的语义较难理解这几条指令在干嘛,翻译成语义信息就是,如果当前 main 函数栈的栈空间不足,则调用 runtime.morestack_noctxt 申请更多栈空间。

接着,继续执行指令 push rbp。在介绍这条指令前,有必要介绍下机器的寄存器,使用 regs 命令查看机器的寄存器:

(dlv) regs
Rip = 0x000000000045feca
Rsp = 0x000000c00003e758
Rax = 0x000000000045fec0
Rbx = 0x0000000000000000
Rcx = 0x0000000000000000
Rdx = 0x00000000004751a0
Rsi = 0x00000000004c3160
Rdi = 0x0000000000000000
Rbp = 0x000000c00003e758
...

机器有很多种寄存器,我们重点关注的是 RipRspRbp 寄存器。

Rip 寄存器中存储的是 CPU 当前执行指令的内存地址,这里要注意,程序中的内存地址为虚拟地址,不存在段地址和偏移地址。当前 Rip 中存储的是 0x000000000045feca,对应执行的机器指令是 => ex0.go:3 0x45feca* 4883ec10 sub rsp, 0x10

Rsp 寄存器一般作为函数栈的栈顶,用来存储函数栈的栈顶地址。Rbp 一般用来存储程序执行的下一条指令,函数栈在跳转时需要知道下一条执行的指令在什么位置(这里不清楚也没关系,后续文章会介绍)

回到 push rbp 指令,该指令会将 rbp 寄存器的值压栈,压栈是从高地址到低地址,Rsp 寄存器将减小 8 个字节。然后 mov rbp, rsp 指令将当前 rsp 寄存器的值赋给 rbp, rbp 将作为函数栈的栈底存在。

根据上述分析,可以画出当前栈的内存空间如下:

继续单步执行 sub rsp, 0x10 指令,rsp 向下减 0x10,这是为 main 函数栈开辟栈空间。rsp 值为:

(dlv) regs
Rsp = 0x000000c00003e748

disass 查看后续执行的汇编指令:

(dlv) disass
Sending output to pager...
TEXT main.main(SB) /root/go/src/foundation/ex0/ex0.go
...
=> ex0.go:4 0x45fece 48c744240801000000 mov qword ptr [rsp+0x8], 0x1
ex0.go:5 0x45fed7 e8e449fdff call $runtime.printlock
ex0.go:5 0x45fedc 488b442408 mov rax, qword ptr [rsp+0x8]
ex0.go:5 0x45fee1 e87a50fdff call $runtime.printint
ex0.go:5 0x45fee6 e8354afdff call $runtime.printunlock
ex0.go:6 0x45feeb 4883c410 add rsp, 0x10
ex0.go:6 0x45feef 5d pop rbp

mov qword ptr [rsp+0x8], 0x10x1 放到 [rsp+0x8] 内存地址中。使用 x 命令可以查看内存地址中的值:

x 0x000000c00003e750
0xc00003e750: 0x01

接着,mov rax, qword ptr [rsp+0x8] 将内存地址 [rsp+0x8]:0x000000c00003e750 的值拷贝到寄存器 rax 中,调用 call $runtime.printint 打印寄存器中的值(这里忽略 call $runtime.printintcall $runtime.printunlock 指令)。

在我们执行下一条指令 add rsp, 0x10 前先看下当前内存空间使用情况。

main 函数栈中 rbp 指向的是函数栈的栈底,rsp 指向的是函数栈的栈顶,在 [rsp+0x8] 的地址存放着局部变量 1。

接着,执行 add rsp, 0x10 回收栈空间:

(dlv) si
> main.main() ./ex0.go:6 (PC: 0x45feef)
ex0.go:6 0x45feeb* 4883c410 add rsp, 0x10
=> ex0.go:6 0x45feef 5d pop rbp (dlv) regs
Rsp = 0x000000c00003e758

要注意,回收只是改变 Rsp 寄存器的值,内存中的数据还是存在的,这是栈段,数据并不会被垃圾回收器回收:

x 0x000000c00003e750
0xc00003e750: 0x01

继续,执行 pop rbp 将原来存储在栈底处的值放到 rbp 寄存器中:

(dlv) regs
Rip = 0x000000000045feef
Rsp = 0x000000c00003e758
Rbp = 0x000000c00003e758 (dlv) si
> main.main() ./ex0.go:6 (PC: 0x45fef0)
ex0.go:6 0x45feef 5d pop rbp
=> ex0.go:6 0x45fef0 c3 ret (dlv) regs
Rip = 0x000000000045fef0
Rsp = 0x000000c00003e760
Rbp = 0x000000c00003e7d0

最后执行 ret 指令退出 main 函数。

至此,我们一个简单的打印局部变量的程序就分析完了。下一篇,我们继续看,如何手写 plan9 汇编。


Go plan9 汇编: 打通应用到底层的任督二脉的更多相关文章

  1. 打通移动App开发的任督二脉、实现移动互联创业的中国梦

    年初的两会上,第一次听到克强总理讲到“互联网+”的计划,当时就让我为之感到无比振奋.我个人的理解是:“互联网+”的本质就是要对传统行业供需双方的重构,通过移动互联技术来推动各个行业上的全民创新,促使中 ...

  2. 深度讲解Linux内存管理和Linux进程调度-打通任督二脉

    我在多年的工程生涯中发现很多工程师碰到一个共性的问题:Linux工程师很多,甚至有很多有多年工作经验,但是对一些关键概念的理解非常模糊,比如不理解CPU.内存资源等的真正分布,具体的工作机制,这使得他 ...

  3. Linux就这个范儿 第13章 打通任督二脉

    Linux就这个范儿 第13章 打通任督二脉 0111010110……你有没有想过,数据从看得见或看不见的线缆上飞来飞去,是怎么实现的呢?数据传输业务的未来又在哪里?在前面两章中我们学习了Linux网 ...

  4. 天河微信小程序入门《三》:打通任督二脉,前后台互通

    原文链接:http://www.wxapp-union.com/forum.php?mod=viewthread&tid=505&extra=page%3D1 天河君在申请到https ...

  5. English learning method ---学英语重中之重打通“任督二脉”

    漫漫十年艰辛路,英语学习之旅 曾经秉承“路漫漫其修远兮,吾将上下而求索”的信念,初一那年了解到原来(a b c d e f g) 不仅仅读作(啊,波,词,的,额,佛,哥),在英语的世界中它有另外的读法 ...

  6. 一个典型的后台软件系统的设计复盘——(三)打通任督二脉-context

    武侠小说练功讲究打通任督二脉.程序设计练到一定程度也讲究打通任督二脉.好奇心强的同学可以搜搜“打通任督二脉有什么感觉”. spring的任督二脉ApplicationContext 最经典的任督二脉莫 ...

  7. Appium+python自动化(十)- 元素定位秘籍助你打通任督二脉 - 上卷(超详解)

    简介 你有道灵光从天灵盖喷出来你知道吗,年纪轻轻就有一身横练的筋骨,简直百年一见的练武奇才啊,如果有一天让你打通任督二脉,那还不飞龙上天啊.正所谓我不入地狱谁入地狱,警恶惩奸维护世界和平这个任务就交个 ...

  8. K2 BPM_当K2遇上医药,用流程打通企业的任督二脉_业务流程管理系统

    据调查,如今仍有60%的医药企业,存在合规经营和利润下降的困扰,在“研”.“产”.“供”.“销”的运营过程中,时时伴随着严苛的管理政策和法规.如何加强企业跨部门.跨组织.跨业务线的执行能力,始终是管理 ...

  9. TCP的慢启动、拥塞避免、重传、快恢复乱七八糟总是记不清?11个连环问让你一次性打通任督二脉

    摘要:如果你的开发过程涉及数据传输,一直在重传.超时之类的方案里有困惑的话,不妨重新学一学可靠性最精致的TCP协议. 本文分享自华为云社区<TCP的慢启动.拥塞避免.重传.快恢复乱七八糟总是记不 ...

  10. 打通MySQL架构和业务的任督二脉

    目前,在很多OLTP场景中,MySQL数据库都有着广泛的应用,也有很多不同的使用方式.从数据库的业务需求.架构设计.运营维护.再到扩容迁移,不同的MySQL架构有不同的特点,适应一定的业务场景,或者解 ...

随机推荐

  1. JS 延迟加载

    function sleep(numberMillis) { var now = new Date(); var exitTime = now.getTime() + numberMillis; wh ...

  2. Git常用命令汇总以及其它相关操作

    --文件目录操作命令 1 mkdir * 创建一个空目录 *指目录名 2 pwd 显示当前目录的路径. 3 cat * 查看*文件内容 4 git rm * 删除**文件 --git初始化操作 1 g ...

  3. 几个适合Java开发者的免费IDEA插件

    今天,给大家推荐几个好用且免费的IntelliJ IDEA插件.如果你还没有用过,可以尝试一下,也许对你的日常工作会有一定的效率提升噢! RestFulTool 如果你是一个RESTful服务的开发者 ...

  4. [rCore学习笔记 014]批处理系统

    写在前面 本随笔是非常菜的菜鸡写的.如有问题请及时提出. 可以联系:1160712160@qq.com GitHhub:https://github.com/WindDevil (目前啥也没有 本章目 ...

  5. ASP.NET Core 程序集注入(三)

    前言: 在Autofac的使用中,提供了个种注入的API其中GetAssemblies()用着特别的舒坦. 1.core2.0也可以使用Autofac的包,但框架自身也提供了默认的注入Api,ISer ...

  6. python 列表append和 的区别??

    python列表中的合并 python列表中append方法是给列表增加一个元素,而'+='是增加上该符号后边的元素,类似于extend方法 不知道对错,先记下来.我学的append方法是在列表最后追 ...

  7. MySQL之DCL

    DCL * 一个项目创建一个用户!一个项目对应的数据库只有一个! * 这个用户只能对这个数据库有权限,其他数据库你就操作不了了! 1. 创建用户   * CREATE USER 用户名@IP地址 ID ...

  8. 第十二节 JMeter基础-中级地址信息【IF控制器】

    声明:本文所记录的仅本次操作学习到的知识点,其中商城IP错误,请自行更改. 背景:提交订单前,我们需要核对一些信息,比如商品信息,收货地址,支付方式等.现在核对一下收货地址信息. 思路: 1.前提条件 ...

  9. idea使用git管理项目(Mac版)

    1.本地安装git mac版 breaw install git 查看是否安装成功 git --version 这样就成功了,一般是自带的有 windows版 https://www.cnblogs. ...

  10. lambda表达式用法

    (参数列表)->{代码块}; (int a,int b)->{return a+b;}; 本质为匿名函数 参数的类型可以省略: (a,b)->{return a+b;} 当参数只有一 ...