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


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. 全志A40i+Logos FPGA开发板(4核ARM Cortex-A7)硬件说明书(下)

    前 言 本文档主要介绍板卡硬件接口资源以及设计注意事项等内容,测试板卡为创龙科技旗下的全志A40i+Logos FPGA开发板. 核心板的ARM端和FPGA端的IO电平标准一般为3.3V,上拉电源一般 ...

  2. [HDCTF 2023]BabyMisc

    BabyMisc ...脑洞坑题(如果7z密码不是那一串超长字符串真不至于0解) 先打开Script.zip,随便打开一个文件夹,得到的是pxx的文件,内容为16进制字节.猜测pxx为对应字节的位置 ...

  3. weui weui-switch 开关取值,设置默认状态

    html <div class="weui-cell__ft"> <input class="weui-switch" type=" ...

  4. Django查询特定条件的数据并插入其他表格模型

    要将特定 wk_nu 值对应的数据批量插入到 MPS005D3Model 中,你可以执行以下步骤: 确定要插入的 wk_nu 值. 获取与该 wk_nu 相关的数据. 将获取的数据逐一创建为 MPS0 ...

  5. [oeasy]python0048_注释_comment_设置默认编码格式

    注释Comment 回忆上次内容 使用了版本控制 git 制作备份 进行回滚   尝试了 嵌套的控制结构 层层 控制   不过 除非 到不得以 尽量不要 太多层次的嵌套   这样 从顶到底 含义 明确 ...

  6. Java JVM——13. 垃圾回收相关算法

    1.生存还是死亡? 在堆里存放着几乎所有的 Java 对象实例,在 GC 执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象.只有被标记为己经死亡的对象,GC 才会在执行垃圾回 ...

  7. LM Studio + open-webui 快速本地部署大语言模型

    目录 一.前言 二.环境准备 三.安装设置 四.下载模型并运行 五.配置 open-webui 写在结尾 一.前言 自 OpenAi 发布 ChatGPT 对话性大语言模型,AI 这两年发展迎来爆发, ...

  8. vs 常用的调试技巧

    本地调试,一般打断点, 然后下一步,或者步入,或者运行到上一步. 有专用的对战窗口. 条件断点,输入当前变量的名称,然后打印变量值变量名和对战的一些信息,当然也可以选择进入断点后是否进一步运行 线程调 ...

  9. 如何正确使用@Bulider与<T>返回数据

    @Data @ToString @Builder @AllArgsConstructor public class PageResult<T> implements Serializabl ...

  10. 全网最适合入门的面向对象编程教程:27 类和对象的Python实现-Python中异常层级与自定义异常类的实现

    全网最适合入门的面向对象编程教程:27 类和对象的 Python 实现-Python 中异常层级与自定义异常类的实现 摘要: 本文主要介绍了在使用 Python 进行面向对象编程时,异常的层级和如何使 ...