教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 golang 没有名字参数_golang内核系列--深入理解plan9汇编&实践

golang 没有名字参数_golang内核系列--深入理解plan9汇编&实践

发布时间:2021-12-20   编辑:jiaochengji.com
教程集为您提供golang 没有名字参数,golang内核系列--深入理解plan9汇编&实践等资源,欢迎您收藏本站,我们将为您提供最新的golang 没有名字参数,golang内核系列--深入理解plan9汇编&实践资源

对于写业务的同学来说,学习汇编可能没必要,仅仅关注业务逻辑即可。
但是当你要深入去优化代码结构、系统架构,就不得不去深入了解golang这门语言,去了解golang内核实现:比如goroutine调度、io调度、map实现、string实现。当然,golang内核有go实现,也有汇编实现。
为了做更深入的优化,我们需要了解plan9汇编,有时候不得不去写汇编,甚至根据特定汇编指令集来做优化。(主要以x86/64架构)

比如我们去看strings.Index实现,其中有一段代码是汇编所写:

<pre class="has"><code>TEXT ·IndexString(SB),NOSPLIT,$0-40 MOVQ a_base 0(FP), DI MOVQ a_len 8(FP), DX MOVQ b_base 16(FP), BP MOVQ b_len 24(FP), AX MOVQ DI, R10 LEAQ ret 32(FP), R11 JMP indexbody<>(SB) TEXT indexbody<>(SB),NOSPLIT,$0 CMPQ AX, DX JA fail CMPQ DX, $16 JAE sse42 no_sse42: CMPQ AX, $2 JA _3_or_more MOVW (BP), BP LEAQ -1(DI)(DX*1), DX </code></pre>

我们不禁会想,golang为何要用汇编来实现这种简单的Index函数?该如何理解?

<h2>plan9汇编简介</h2>

主要包括:文件命名、指令集、寄存器、函数声明、全局变量声明、跳转、栈分布、调用栈、编译/反编译工具

<h2>文件命名</h2>

由于不同的平台架构支持的汇编指令集不一样,需要针对不同的架构写不同的汇编实现。
通常文件命名格式:<i>功能名_arch.s</i>
比如:<i>indexbyte_386.s, indexbyte_arm64.s, indexbyte_s390x.s</i>
使用go build编译的时候,会自动根据当前arch平台使用对应的arch文件(或者使用 build tag)

<h2>指令集</h2>

这里自行去查找不同的架构指令集即可(cat /proc/cpuinfo | grep flags | head -1)

<h2>寄存器</h2>

有4个核心的伪寄存器,这4个寄存器是编译器用来维护上下文、特殊标识等作用的:
FP(Frame pointer): arguments and locals
PC(Program counter): jumps and branches
SB(Static base pointer): global symbols
SP(Stack pointer): top of stack

所有用户空间的数据都可以通过FP/SP(局部数据、输入参数、返回值)和SB(全局数据)访问。 通常情况下,不会对SB/FP寄存器进行运算操作,通常情况以会以SB/FP/SP作为基准地址,进行偏移解引用 等操作。

其中1: SP有伪SP和硬件SP的区分:伪SP: 本地变量最高起始地址硬件SP: 函数栈真实栈顶地址
他们的关系是:

<ul><li>如果没有本地变量:伪SP=硬件SP 8</li><li>如果有本地变量:伪SP=硬件SP 16 本地变量空间大小</li></ul>

2: FP伪寄存器FP伪寄存器: 用来标识函数参数、返回值
和伪SP寄存器的关系是:

<ul><li>如果有本地变量或者栈调用存严格split在关系(无NOSPLIT),伪FP=伪SP 16</li><li>否则,伪FP=伪SP 8</li><li>FP是访问入参、出参的基址,一般用正向偏移来寻址,SP是访问本地变量的起始基址,一般用负向偏移来寻址</li><li>修改硬件SP,会引起伪SP、FP同步变化</li></ul>
<pre class="has"><code>SUBQ $16, SP // 这里golang解引用时,伪SP/FP都会-16 </code></pre>

3: 参数/本地变量访问通过symbol /-offset(FP/SP)的方式进行使用
例如arg0 0(FP)表示第函数第一个参数其实的位置,arg1 8(FP)表示函数参数偏移8byte的另一个参数。arg0/arg1用于助记,但是必须存在,否则无法通过编译(golang会识别并做处理)。其中对于SP来说,还有一种访问方式: /-offset(FP)
这里SP前面没有symbol修饰,代表这硬件SP

4: PC寄存器
实际上就是在体系结构的知识中常见的pc寄存器,在x86平台下对应ip寄存器,amd64上则是rip。除了个别跳转 之外,手写代码与PC寄存器打交道的情况较少。

5: SB寄存器
SP是栈指针寄存器,指向当前函数栈的栈顶,通过symbol offset(SP)的方式使用。offset 的合法取值是 [-framesize, 0),注意是个左闭右开的区间。假如局部变量都是8字节,那么第一个局部变量就可以用localvar0-8(SP) 来表示。

6: BP寄存器
还有BP寄存器,是表示已给调用栈的起始栈底(栈的方向从大到小,SP表示栈顶);一般用的不多,如果需要做手动维护调用栈关系,需要用到BP寄存器,手动split调用栈

7: 通用寄存器
在plan9汇编里还可以直接使用的amd64的通用寄存器,应用代码层面会用到的通用寄存器主要是: rax, rbx, rcx, rdx, rdi, rsi, r8~r15这14个寄存器。plan9中使用寄存器不需要带r或e的前缀,例如rax,只要写AX即可:

<pre class="has"><code>MOVQ $101, AX </code></pre>

代码示例:

<pre class="has"><code>func Add(a, b int) (c int) { sum := 0 return a b sum } </code></pre>

各个变量通过寄存器解引用如下:(伪FP=伪SP 16=硬件SP 24)

<ul><li>a:a 0(SP)或者a 16(SP)</li><li>b:b 8(SP)或者a 24(SP)</li><li>c:c 16(SP)或者a 32(SP)</li><li>sum:sum-8(SP)或者a-24(FP)</li></ul><h2>函数声明</h2>

此处声明了一个函数sqrt,函数的声明以 TEXT 标识开头,以 {package}·{function} 为函数名。 如何函数属于本package时,通常可以不写{package},只留·{function}即可。· 在mac上可以用shift option 9 打出。$0表示该函数栈大小为0byte,计算栈大小时,需要考虑局部变量和本函数内调用其他函数时,需要传参的空间,不含函数返回地址和CALLER BP。 $16表示该函数入参和返回值一共有16byte。当有NOSPLIT标识时,可以不写输入参数、返回值占用的大小(这时候会强行插入CALLER BP)。

为了在golang代码里能引用这个函数我们需要做如下申明:

<pre class="has"><code>import math func sqrt(float64) float64 </code></pre>
<h2>全局变量声明</h2>

全局变量的数据部分采用DATA symbol offset(SB)/width, value格式进行声明。<>表示该变量只在该文件内全局可见。

<pre class="has"><code>DATA divtab<> 0x00(SB)/4, $0xf4f8fcff // divtab的前4个byte为0xf4f8fcff DATA divtab<> 0x04(SB)/4, $0xe6eaedf0 // divtab的4-7个byte为0xe6eaedf0 ... DATA divtab<> 0x3c(SB)/4, $0x81828384 // divtab的最后4个byte为0x81828384 GLOBL divtab<>(SB), RODATA, $64 // 全局变量名声明,以及数据所在的段"RODATA",数据的长度64byte </code></pre>
<h2>跳转</h2>

跳转分为section跳转或者函数调用跳转

<ul><li>section跳转 <ul><li>类似JNE,JBE,JE,JGE等;其中sp/bp不会变化;栈空间不变,不存在参数传递需求</li></ul></li></ul><ul><li>函数调用跳转 <ul><li>JMQ sp/bp不会变化;栈空间不变。通常需要调用者和被调用者协商好使用那些寄存传递参数,调用者将参数写入这些寄存器</li><li>CALL 栈空间会发生响应的变化,传递参数时,我们需要输入参数、返回值按之前将的栈布局安排在调用者的栈顶(低地址段),然后再调用CALL命令来调用其函数,调用CALL命令后,SP寄存器会下移一个WORD(x86_64上是8byte),然后进入新函数的栈空间运行</li></ul></li></ul><h2>栈分布</h2>

如果没有本地变量,栈分布如下

<pre class="has"><code>func xxx(a, b, c int) (e, f, g int) { e, f, g = a, b, c return } </code></pre>

如果有本地变量,栈分布如下 ```javascript func zzz(a, b, c int) [3]int{ var d [3]int d[0], d[1], d[2] = a, b, c return d } ```

<h2>调用栈</h2>

这里以一个函数调用过程A->B->C为例了来解释调用栈过程

<h2>编译/反编译工具</h2>

实践出真知,很多时候我们无法确定一块代码是如何执行的,需要通过生成汇编、反汇编来研究golang。这里给一些工具来帮助我们了解golang

<pre class="has"><code>// 编译 go build -gcflags="-S" go tool compile -S hello.go go tool compile -N -S hello.go // 禁止优化 // 反编译 go tool objdump <binary> </code></pre>
<h2>汇编实战</h2>

纸上得来终觉浅,绝知此事要躬行。只有亲自写汇编代码才能帮助我们更好的了解汇编。

我们分别从2部分实践:

<ul><li>了解SP、FP的关系,输入、输出</li><li>做一个简单的函数,实现手动管理栈</li></ul><h2>练习一:分别获取当前伪SP、硬件SP、伪FP地址</h2>

思路:使用LEA把各个寄存器的地址传出来即可

<pre class="has"><code>TEXT ·SpFp(SB),NOSPLIT,$0-24 LEAQ (SP), AX LEAQ a 0(SP), BX LEAQ b 0(FP), CX MOVQ AX, ret 0(FP) MOVQ BX, ret 8(FP) MOVQ CX, ret 16(FP) RET </code></pre>

输出如下: <i>824634359232, 824634359232, 824634359240</i>
可以看到没有本地变量的情况下:伪SP=硬件SP=FP-8
思考:如果把中间的NOSPLIT去掉结果如何?如果把$0-24改成$8-24结果如下?

<h2>练习二:使用汇编实现斐波那契</h2>

思路:使用递归调用;扩容栈16个字节用来存储递归调用所需的输入和输出

<pre class="has"><code>TEXT ·Fab(SB),$0-16 MOVQ n 0(FP), AX CMPQ AX, $1 JBE end SUBQ $16, SP MOVQ AX, 0(SP) DECQ 0(SP) CALL ·Fab(SB) MOVQ 8(SP), AX MOVQ AX, 40(SP) DECQ 0(SP) CALL ·Fab(SB) MOVQ 8(SP), AX ADDQ AX, 40(SP) ADDQ $16, SP RET end: MOVQ $1, ret 8(FP) RET </code></pre>

整个调用栈管理如下,这里用16字节是为了节省栈空间(2次调用输入输出参数共用同一块地址)

<h2>总结</h2>

golang汇编难点在于掌握几个寄存器的关系以及栈分布以及调用栈的过程(无关今紧要的没讲)。想要深入理解,需要多实践。最终目的不一定要熟练写汇编代码,更多的是懂得golang底层机制以及学习人家优化思想。

<ul><li>相关参考文献(相关资料很少,主要参考下面,有些介绍不详细,文中都做了重点标注)https://lrita.github.io/images/posts/go/GoFunctionsInAssembly.pdfhttps://lrita.github.io/2017/12/12/golang-asm/#go函数调用</li><li>代码地址https://github.com/buptbill220/gotls/tree/master/interesting</li></ul>
到此这篇关于“golang 没有名字参数_golang内核系列--深入理解plan9汇编&amp;实践”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
golang 没有名字参数_golang内核系列--深入理解plan9汇编&amp;实践
走进Golang之运行与Plan9汇编
小米技术出品——走进Golang之编译器原理
编程中文书集合
golang汇编语言基础
golang sdk后端怎么用_Golang资深后端工程师需要了解的知识点
Goroutine并发调度模型深度解析&amp;手撸一个协程池
为什么要学 Go
理解 Golang 中函数调用的原理
Golang笔记:语法,并发思想,web开发,Go微服务相关

[关闭]
~ ~