go-asm-反汇编
了解底层才能理解程序的运作方式,了解汇编是深入了解Go的必经之路。
平台: mac intel
Go: 1.17
文中所用代码见附录
基础
段(section)
程序编译以后会分为不同的段,一般来说会分为
- .text 代码段 存放程序代码,通常是只读的
- .data 数据段 存放初始化了的全局变量和初始化了的静态变量
- .bss 全局变量段 存放未初始化的全局变量和未初始化的静态变量
名字都是历史定的,也没啥特殊硬记就完事。主要就是把数据按照不同的分类放在一起。也就是说可以自定义段信息来区分,能找到对应的数据就行。
查看段信息
查看段信息有几种方式
readelf
readelf -S 这个只支持elf格式的
1 | GOOS=linux go build -gcflags="-B -N -l" main.go // 编译linux平台 // -B不开启越界检测 -N关闭优化 -l关闭内联 |
可以得到以下内容
1 | There are 23 section headers, starting at offset 0x1c8: |
objdump
objdump -h 支持各种目标文件 这个mac需要brew install binutils
安装一下
1 | go build -gcflags="-B -N -l" main.go |
可以得到以下内容
1 | main: file format mach-o-x86-64 |
反汇编
go查看汇编有几种方式:
- go tool compile -S xxx.go
- go build -gcflags=”-S” xxx.go -gcflags其实就是把参数传给compile
- 先go build 编译,然后 go tool objdump -S xxx
- objdump -d
1/2 都是直接编译为汇编,生成的是过程中的机器码汇编。
3/4属于反汇编,生成是最终机器码汇编。
注意不管是生成的还是反汇编的,SP都是硬件SP,与手写汇编不一致。
这里我们以查看使用的string具体怎么存储,怎么读取的为例。
过程中的汇编
1 | go tool compile -B -N -l -S main.go |
裁掉不关注信息可以得到
1 | "".main STEXT size=397 args=0x0 locals=0xc0 funcid=0x0 |
S(ymbol)开头的表示符号类型 具体可见SymbolKind
DATA 表示数据,RODATA 表示只读数据
TEXT 表示符号
dupok 表示数据只有一份
解析
挑重点看一下
符号定义
"".main STEXT size=397 args=0x0 locals=0xc0 funcid=0x0
“”.main 是符号名
STEXT 表示符号是函数
size为占用大小
args参数大小
locals局部变量大小
funcid表示函数类型0为普通函数,具体可见funcid
下方跟了一段反编译代码
反编译代码后为原始二进制编码
再下方为引用的数据,简单看一个
rel 140+4 t=15 "".svz+0
分别为 rel 偏移+长度 t=重定位类型(具体定义见RelocType) 符号名+偏移量
140号位置也就是(0x8c)长度为4字节的空间为””.svz+0的数据引用
可以具体位置0x0080 00 48 89 d9 e8 00 00 00 00 48 8d 15 00 00 00 00 .H.......H......
最后四子节全为0,因为是引用数据
字符串定义
1
2go.string."svz-svz" SRODATA dupok size=7
0x0000 73 76 7a 2d 73 76 7a svz-svzgo.string.”svz-svz” 符号名
SRODATA 表示符号是只读数据
dupok 表示只有一份
size 为占用大小
变量定义
1
2
3"".svz SDATA size=16
0x0000 00 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 ................
rel 0+8 t=1 go.string."svz-svz"+0“”.svz 符号名
SDATA 表示符号是数据
size 为占用大小 16是因为字符串底层为 data指针+长度
先看后8字节 07 00 00 00 00 00 00 00 因为是小端,实际为00 00 00 00 00 00 00 07 也就是7
再看下方的rel
0号位置长度为8字节的空间 为go.string.”svz-svz”+0的数据引用
过程中的就只能看到这么多信息了。
最终的汇编
先看go tool的版本
1 | go tool objdump -gnu -s main.main main // -gnu 同时输出gnu风格的汇编 -s 匹配指定符号名 |
裁出来main函数汇编如下
1 | TEXT main.main(SB) /Users/svz/code/test/test/main.go |
这里生成的就带上了具体地址信息了
把使用字符串的挑出来看
main.go:22 0x108b886 488d1503b70a00 LEAQ main.svz(SB), DX // lea 0xab703(%rip),%rdx
可以看到把main.svz(SB)的地址放到了DX,1.17之后为寄存器传参
对应的gnu汇编则是将0xab703(%rip)地址放到了%rdx,0xab703(%rip)换算过来就是0x1136f90,对比运行结果就是变量svz的地址
对于go tool来说,也只能看到这一层了,想要再深一点就得上objdump了
下面来用objdump来看
1 | objdump -d -j .text main |
截取main.main部分
1 | 000000000108b860 <_main.main>: |
还是捞出使用string的
1 | 108b886: 48 8d 15 03 b7 0a 00 lea 0xab703(%rip),%rdx # 1136f90 <_main.svz> |
可以看到和go tool的地址一致,也就是0x1136f90,这里直接帮忙计算出来了,免得自己算
通过上方头信息可以得知1136f90在data段
再来看data段信息
1 | objdump -s -j .data main |
截取1136f90
的部分
1 | 1136f90 ae380a01 00000000 07000000 00000000 .8.............. |
可以看到16字节内容,其实就是变量svz的内容
拆成两段,
ae380a01 00000000
小端模式转换出来就是 010a38ae
对比程序输出就是实际字符位置
07000000 00000000
小端模式转换出来就是7
,也就是长度
通过上方头信息得知010a38ae
在 __TEXT.__rodata
段
再来看__TEXT.__rodata
段信息
1 | objdump -s -j __TEXT.__rodata main |
裁出来010a38ae
部分
1 | 10a38a0 72756e6e 696e6773 69676e61 6c207376 runningsignal sv |
可以看到010a38ae
部分为svz-svz
即我们给的值
附录
示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26/**
* @file main.go
* @author 903943711@qq.com
* ___ _ _ ____
* / __)( \/ )(_ )
* \__ \ \ / / /_
* (___/ \/ (____)
* @date 2021/9/8
* @desc
*/
package main
import (
"fmt"
"reflect"
"unsafe"
)
var svz = "svz-svz"
func main() {
fmt.Println(&svz)
trueV := (*reflect.StringHeader)(unsafe.Pointer(&svz)).Data
fmt.Printf("0x%x | %v\n", trueV, trueV)
}