Go plan9 汇编: 打通应用到底层的任督二脉

news/2024/11/17 2:49:18/文章来源:https://www.cnblogs.com/xingzheanan/p/18390537

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


0. 前言

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

1. Go 程序到汇编

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

package mainfunc main() {a := 1print(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,$-8JMP	_rt0_amd64(SB)TEXT _rt0_amd64_linux_lib(SB),NOSPLIT,$0JMP	_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,$-8MOVQ	0(SP), DI	// argcLEAQ	8(SP), SI	// argvJMP	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 main2:
=>   3: func main() {4:         a := 15:         print(a)6: }

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

(dlv) disass
TEXT main.main(SB) /root/go/src/foundation/ex0/ex0.goex0.go:3        0x45fec0        493b6610                cmp rsp, qword ptr [r14+0x10]ex0.go:3        0x45fec4        762b                    jbe 0x45fef1ex0.go:3        0x45fec6        55                      push rbpex0.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) regsRip = 0x000000000045fecaRsp = 0x000000c00003e758Rax = 0x000000000045fec0Rbx = 0x0000000000000000Rcx = 0x0000000000000000Rdx = 0x00000000004751a0Rsi = 0x00000000004c3160Rdi = 0x0000000000000000Rbp = 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 将作为函数栈的栈底存在。

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

image

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

(dlv) regsRsp = 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], 0x1ex0.go:5        0x45fed7        e8e449fdff              call $runtime.printlockex0.go:5        0x45fedc        488b442408              mov rax, qword ptr [rsp+0x8]ex0.go:5        0x45fee1        e87a50fdff              call $runtime.printintex0.go:5        0x45fee6        e8354afdff              call $runtime.printunlockex0.go:6        0x45feeb        4883c410                add rsp, 0x10ex0.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 前先看下当前内存空间使用情况。

image

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) regsRsp = 0x000000c00003e758

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

x 0x000000c00003e750 
0xc00003e750:   0x01

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

(dlv) regsRip = 0x000000000045feefRsp = 0x000000c00003e758Rbp = 0x000000c00003e758(dlv) si
> main.main() ./ex0.go:6 (PC: 0x45fef0)ex0.go:6        0x45feef        5d              pop rbp
=>      ex0.go:6        0x45fef0        c3              ret(dlv) regsRip = 0x000000000045fef0Rsp = 0x000000c00003e760Rbp = 0x000000c00003e7d0

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

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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/790164.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

20221414徐鹿鸣Markdown学习作业

问题一:哪些内容是你掌握的?哪些内容是你没有掌握的?使用AI推荐的工具或者你喜欢的工具实践一下没有掌握的内容。通义千问的回复 对我而言,Markdown的基础语法我接触过一二,但余下的高级语法、各种工具以及它与人工智能生成的内容的关联我并不清楚。 StackEdit在线Markdow…

Qt svg 图标图片不显示

因为少了Qt5Svg.dll库和imageformats文件夹的依赖,可以对对应Qt安装目录下的32位或者64位库文件夹中去找到。 自己开发了一个股票智能分析软件,功能很强大,需要的关注微信公众号:QStockView

深度学习(初始化)

深度学习训练的时候,一个好的初始化结果能使模型更容易收敛,Pytorch提供了一些初始化函数。import torch import torch.nn as nn import matplotlib.pyplot as pltclass Model(nn.Module):def __init__(self):super(Model, self).__init__()self.a = nn.Parameter(torch.zero…

电流峰值采样电路分析

一、前言在一些电路控制中,采集电路中某部分的峰值电流做处理是非常有必要的,所谓的电流采样,本质上还是电压采样。接下来将介绍一种电流峰值采样电路。 二.基本原理最基本的原理是利用电容的充放电特性,基本电路如下图所示,当电源电压上升时,二极管导通给电容充电。电源…

自我介绍和软工五问

| 这个作业属于哪个课程 | https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/ | | ----------------- |--------------- | | 这个作业要求在哪里| https://edu.cnblogs.com/campus/gdgy/CSGrade22-34/homework/13228 | | 这个作业的目标 | <学习使用github,思考自己对课程…

信息学奥赛初赛天天练-80-NOIP2015普及组-基础题5-错位排列、二叉树、完全二叉树、叶子节点、完全二叉树叶子节点

NOIP 2015 普及组 基础题5 21 重新排列 1234使得每一个数字都不在原来的位置上,一共有( )种排法 22 一棵结点数为 2015的二叉树最多有( )个叶子结点 2 相关知识点 1) 错位排列 考虑一个有n个元素的排列,若一个排列中所有的元素都不在自己原来的位置上,那么这样…

2024 NepCTF

NepCTF NepMagic —— CheckIn 直接玩游戏就能出 注意有一关要把隐藏的方块全找到 NepCamera 先使用tshark读取数据结果文件中发现大量jpeg头ffd8ffe0。猜测是多个图片,编写脚本,提取出来。 脚本: import reinputFilePath="C:\\Downloads\\NepCamera\\result.txt"w…

JAVA基础之三-接口和抽象类

java提供了抽象类和接口,总体是好事。 有的OOP语言并没有接口的概念,但相当一部分其实用其它方式实现了JAVA中接口类似的功能。 如果不太清楚二者的区别,难免在面临具体业务的时候,在二者之间摇摆。 --- 实际上,关于抽象类和接口的共同点和不同点没有什么可以写的。 设计原…

iMac安装Windows系统键盘无反应

热烈欢迎,请直接点击!!! 进入博主App Store主页,下载使用各个作品!!! 注:博主将坚持每月上线一个新app!! 1、鼠标右击任务栏空白处,选择“任务管理器”: 2、在进程里边找到“Microsoft IME”,右键点击它,选择“结束任务”

spark的SparkSubmit类关于Configuration的资源文件加载

在阅读 SparkSubmit 源代码时,重点关注 Configuration 的资源文件的加载情况,默认通过 new Configuration() 构造方法创建时,只会加载 core-default.xml 和core-site.xml文件,但是 SparkSubmit 中打印 Configuration 时,发现还会加载 yarn-site.xml,SparkSubmit 代码中没…

uni-app实录

虽然小程序已经火了好几年,但是现在还是经久不衰,虽然已经有很多出色的制作者和供应商,但作为一门技术手艺,秉持着“技多不压身”的原则,这里再次进行填坑。本文来自博客园,作者:ukyo--君君小时候,转载请注明原文链接:https://www.cnblogs.com/ukzq/p/18390418

python中的编码解码

https://cloud.tencent.com/developer/article/2278351 编码(encode):将Unicode字符串转为特定编码格式对应的字节码的过程;就是将字符串转换为字节码 解码(decode):将特定编码格式的字节码转为对应的Unicode字符串的过程;就是将字节码转换为字符串 正确写法只有str.en…