- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
原创文章,欢迎转载,转载请注明出处,谢谢.
作为一个严肃的 Gopher,了解汇编是必须的。本汇编系列文章会围绕基本的 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 汇编翻译之后的汇编.
本文的重点并不是单步调试 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
...
机器有很多种寄存器,我们重点关注的是 Rip,Rsp 和 Rbp 寄存器.
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], 0x1 将 0x1 放到 [rsp+0x8] 内存地址中。使用 x 命令可以查看内存地址中的值:
x 0x000000c00003e750
0xc00003e750: 0x01
接着,mov rax, qword ptr [rsp+0x8] 将内存地址 [rsp+0x8]:0x000000c00003e750 的值拷贝到寄存器 rax 中,调用 call $runtime.printint 打印寄存器中的值(这里忽略 call $runtime.printint 和 call $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 汇编.
最后此篇关于Goplan9汇编:打通应用到底层的任督二脉的文章就讲到这里了,如果你想了解更多关于Goplan9汇编:打通应用到底层的任督二脉的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在通过 labrepl 工作,我看到了一些遵循此模式的代码: ;; Pattern (apply #(apply f %&) coll) ;; Concrete example user=> (a
我从未向应用商店提交过应用,但我会在不久的将来提交。 到目前为止,我对为 iPhone 而非 iPad 进行设计感到很自在。 我了解,通过将通用PAID 应用放到应用商店,客户只需支付一次就可以同时使
我有一个应用程序,它使用不同的 Facebook 应用程序(2 个不同的 AppID)在 Facebook 上发布并显示它是“通过 iPhone”/“通过 iPad”。 当 Facebook 应用程序
我有一个要求,我们必须通过将网站源文件保存在本地 iOS 应用程序中来在 iOS 应用程序 Webview 中运行网站。 Angular 需要服务器来运行应用程序,但由于我们将文件保存在本地,我们无法
所以我有一个单页客户端应用程序。 正常流程: 应用程序 -> OAuth2 服务器 -> 应用程序 我们有自己的 OAuth2 服务器,因此人们可以登录应用程序并获取与用户实体关联的 access_t
假设我有一个安装在用户设备上的 Android 应用程序 A,我的应用程序有一个 AppWidget,我们可以让其他 Android 开发人员在其中以每次安装成本为基础发布他们的应用程序推广广告。因此
Secrets of the JavaScript Ninja中有一个例子它提供了以下代码来绕过 JavaScript 的 Math.min() 函数,该函数需要一个可变长度列表。 Example:
当我分别将数组和对象传递给 function.apply() 时,我得到 NaN 的 o/p,但是当我传递对象和数组时,我得到一个数字。为什么会发生这种情况? 由于数组也被视为对象,为什么我无法使用它
CFSDN坚持开源创造价值,我们致力于搭建一个资源共享平台,让每一个IT人在这里找到属于你的精彩世界. 这篇CFSDN的博客文章ASP转换格林威治时间函数DateDiff()应用由作者收集整理,如果你
我正在将列表传递给 map并且想要返回一个带有合并名称的 data.frame 对象。 例如: library(tidyverse) library(broom) mtcars %>% spl
我有一个非常基本的问题,但我不知道如何实现它:我有一个返回数据框,其中每个工具的返回值是按行排列的: tmp<-as.data.frame(t(data.frame(a=rnorm(250,0,1)
我正在使用我的 FB 应用创建群组并邀请用户加入我的应用群组,第一次一切正常。当我尝试创建另一个组时,出现以下错误: {"(OAuthException - #4009) (#4009) 在有更多用户
我们正在开发一款类似于“会说话的本”应用程序的 child 应用程序。它包含大量用于交互式动画的 JPEG 图像序列。 问题是动画在 iPad Air 上播放正常,但在 iPad 2 上播放缓慢或滞后
我关注 clojure 一段时间了,它的一些功能非常令人兴奋(持久数据结构、函数式方法、不可变状态)。然而,由于我仍在学习,我想了解如何在实际场景中应用,证明其好处,然后演化并应用于更复杂的问题。即,
我开发了一个仅使用挪威语的应用程序。该应用程序不使用本地化,因为它应该仅以一种语言(挪威语)显示。但是,我已在 Info.plist 文件中将“本地化 native 开发区域”设置为“no”。我还使用
读完 Anthony's response 后上a style-related parser question ,我试图说服自己编写单体解析器仍然可以相当紧凑。 所以而不是 reference ::
multicore 库中是否有类似 sapply 的东西?还是我必须 unlist(mclapply(..)) 才能实现这一点? 如果它不存在:推理是什么? 提前致谢,如果这是一个愚蠢的问题,我们深表
我喜欢在窗口中弹出结果,以便更容易查看和查找(例如,它们不会随着控制台继续滚动而丢失)。一种方法是使用 sink() 和 file.show()。例如: y <- rnorm(100); x <- r
我有一个如下所示的 spring mvc Controller @RequestMapping(value="/new", method=RequestMethod.POST) public Stri
我正在阅读 StructureMap关于依赖注入(inject),首先有两部分初始化映射,具体类类型的接口(interface),另一部分只是实例化(请求实例)。 第一部分需要配置和设置,这是在 Bo
我是一名优秀的程序员,十分优秀!