freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

扫描器开发小插曲--编译相关
2022-06-08 15:56:50
所属地 北京

前言

原本要说一下扫描器中写代理和用代理的诸多坑,但是其中有一些地方涉及到一点前置知识就先在这里说一下,并稍微扩展一点

主要说一下这几个问题,都比较重要

  1. go的基本编译命令
  2. go编译出来的东西为什么体积较大
  3. 动态编译和静态编译

go 的基本编译命令

最基础的命令:在项目文件夹中使用,会自动查找main包中的main函数作为入口

go build

只有在测试的时候建议使用,实际要用的时候不要使用,一旦报错会爆出调试信息(也就是变异时候文件的绝对路径,会带出你的主机名),建议使用下面的

go build -ldflags "-s -w"

-s去掉符号表信息,panic时候的stack trace就没有任何文件名/行号信息了

-w去掉DWARF的调试信息,得到的程序就不能用gdb调试了

坑:当我们从github上拉下来一个项目之后,如果项目用的包管理器是gomod,我们使用上面的编译命令完全没有问题,但如果他使用的是gopath,上面的命令就需要变一下,我们需要指定一下入口函数的位置 go build main.go这样。gomod包管理器会在项目文件多多一个go.mod的文件,里面写了项目需要引用的第三方库及版本。使用了gomod包管理器会让项目更灵活,可以将项目创建在任意文件夹,编译的时候直接go build即可,现在大部分项目都用的gomod,从gopath转换到gomod也很简单先后运行go mod init和go mod tidy,然后在go build就可以了

跨平台编译

这里以mac跨平台编译为例

在此之前我们可以看一下go的环境变量go env

GO111MODULE=""

GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/username/Library/Caches/go-build"
GOENV="/Users/username/Library/Application Support/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/username/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/username/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/Cellar/go/1.16.3/libexec"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/Cellar/go/1.16.3/libexec/pkg/tool/darwin_amd64"
GOVCS=""
GOVERSION="go1.16.3"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/Users/username/go/src/Webstudy/sotest/go.mod"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/_f/sw7zdg617471nczsfnnghkvr0000gn/T/go-build2049360732=/tmp/go-build -gno-record-gcc-switches -fno-common"

跨平台编译中,我们使用到两个环境变量

GOOS="darwin"
GOARCH="amd64"

大概可以猜到,这两个字段的目的GOOS指定操作系统darwin是mac,可指定windows和linux,GOARCH指定架构,64位是amd64、32位是386。还支持很多操作系统版本使用go tool dist list

aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
illumos/amd64
ios/amd64
ios/arm64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
openbsd/mips64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
windows/386
windows/amd64
windows/arm

我们可以通过设置这个两个环境变量来实现跨平台编译,比如我们现在要在mac上编译linux的版本

go env -w GOOS="linux"  
go build -ldflags "-s -w"

这样就可以编译出linux版本

windows同理

go env -w GOOS="windows"  
go build -ldflags "-s -w"

如果还有架构要求可以加上

go env -w GOOS="windows"
go env -w GOARCH="386"
go build -ldflags "-s -w"

在mac和linux上还可以在在编译的时候临时更改环境变量,直接一条命令跨平台编译

GOOS=windows GOARCH=386 go build  -ldflags "-s -w" 
GOOS=linux GOARCH=amd64 go build  -ldflags "-s -w" 

go编译出来的东西为什么这么大

这其实既是优点又是缺点,缺点就是大,优点就是他跨平台

go程序大的原因就是它独立实现了runtime,所有的Go二进制文件中都包含Go runtime,以及支持动态类型检查,反射甚至禁止时间堆栈跟踪所必须的runtime类型信息

举个最简单的例子

package main

import "fmt"


func main() {
fmt.Println("hello")
}

我们仅仅一个打印hello的操作,编译出来之后,就有1.5M,

-rw-r--r--@ 1 root  staff   6.0K Jan  4 17:20 .DS_Store
-rw-r--r--  1 root  staff    32B Jan  5 11:26 go.mod
-rw-r--r--  1 root  staff    66B Jan  5 14:45 main.go
-rwxr-xr-x  1 root  staff   1.5M Jan  5 14:45 sotest

那我们在输出一行

package main

import "fmt"


func main() {
fmt.Println("hello")
fmt.Println("world")
}

编译出来其实也是1.5M

-rw-r--r--@ 1 root  staff   6.0K Jan  4 17:20 .DS_Store
-rw-r--r--  1 root  staff    32B Jan  5 11:26 go.mod
-rw-r--r--  1 root  staff    87B Jan  5 14:48 main.go
-rwxr-xr-x  1 root  staff   1.5M Jan  5 14:48 sotest

runtime会被打包进go程序中,所以第二次的大小基本上就是增加了一行字符串的大小

再其次这个1.5M中还有fmt及其依赖包的大小,去掉fmt包

package main


func main() {
println("hello")
}
-rw-r--r--@ 1 root  staff   6.0K Jan  4 17:20 .DS_Store
-rw-r--r--  1 root  staff    32B Jan  5 11:26 go.mod
-rw-r--r--  1 root  staff    48B Jan  5 14:54 main.go
-rwxr-xr-x  1 root  staff   868K Jan  5 14:54 sotest

也就800多k,同样的c代码编译出来是48k!

#include
int main() {
    printf("%s\n", "hello");
    return 0;
}
-rwxr-xr-x  1 root  staff    48K Jan  5 15:03 hello

所以,归根结底go程序比较大的原因:

  • 微观上:是因为所有的go程序都包含自己独立实现的runtime,所以会大一点
  • 宏观上:go的第三方库,因为都是自己实现的,所以大小在0到几M不等

在其次,操作系统是用c写的,对于c写的程序来说,大部分的依赖都可以调用动态链接库,所以降低了程序本身大小。这也导致跨平台性比较差,同样的程序可能不支持跨平台编译。如果编译c的时候使用静态连接会发现大小其实和go差的不是很多

runtime

runtime是支撑程序运行的基础,做常见的就是libc(c的runtime),他是目前主流操作系统上最普遍的runtime。但是他由于实现时间较早,版本参差不齐,对于并发的支持不是很好,这样的runtime并不能满足go的需求。综合大部分因素,go自己实现了runtime并封装了syscall,为不同平台上的go user level代码提供封装完成的、统一的go标准库;同时Go runtime实现了对goroutine模型的支持。

如何降低go二进制程序大小

1 对于一个没写完的程序

根本减少体积的方式就是不要使用太多的第三方库,原本一个很简单的功能就不要使用第三方库,因为你想要实现的功能仅仅是这个第三方库的冰山一角,但因为这一角把整座冰山搬回来完全没必要。

可以自己写,还可以将想要的代码复制回来,这样可以有效的减少体积

2编译的时候如何减少体积

最常用的方式上面已经说过了,利用参数去掉一些调试信息,同样的程序可以减少大概百分之30的体积

go build -ldflags "-s -w" -o 111


-rwxr-xr-x  1 root  staff   2.0M Jan  5 15:43 111




go build -o 222 


-rwxr-xr-x  1 root  staff   2.6M Jan  5 15:43 222

还有一种就是动态编译和静态编译会影响大小,但是不大可以忽略下面会讲

最麻烦的方法是使用gccgo动态编译,自行探索,效果是最好的但是也最麻烦的!

3编译后减少体积

使用upx压缩,效果非常好

-rwxr-xr-x  1 root  staff   2.0M Jan  5 15:43 111   // 加参数编译
-rwxr-xr-x  1 root  staff   2.6M Jan  5 15:43 222   // 直接编译




upx -9 -o 333 111      // 压缩加参数编译


-rwxr-xr-x  1 root  staff   2.0M Jan  5 15:43 111
-rwxr-xr-x  1 root  staff   2.6M Jan  5 15:43 222
-rwxr-xr-x  1 root  staff   792K Jan  5 15:43 333

直接缩减到792k减少了%120多,当然这是这个程序的效果,实际在%100左右,就大概压缩一半的体积

坑:对于正常使用的程序没问题,但是如果是要上传懂啊目标机器的程序,会被部分杀软杀掉,所以要上传到目标机器运行的程序不要使用upx压缩

动态编译和静态编译

同样是上面的例子,一个go程序一个c程序

#include
int main() {
    printf("%s\n", "hello");
    return 0;
}
package main

import "fmt"


func main() {
fmt.Println("hello")
}

编译之后,得到来给你个功能完全一样语言不通的两个二进制文件

-rwxr-xr-x  1 root root   16040  1月  5 16:49 print_c
-rwxr-xr-x  1 root root 1766422  1月  5 16:47 print_go

马上就会知道为什么c那么小了,我们使用readref工具,即可看到这个程序的直接依赖库

┌──(root kali)-[~/print]
└─# readelf -a print_c | grep NEEDED
 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]


┌──(root kali)-[~/print]
└─# readelf -a print_go | grep NEEDED


┌──(root kali)-[~/print]

当然这是直接依赖库,还有间接的,这里就不列了,可以看到,c会有依赖库,而go没有依赖库,这也是go比c大的原因之一,也是为什么go跨平台的原因,如果换了其他平台或者c程序的一个依赖库缺少或改变c程序就没办法跑起来

还可以使用nm查看到哪些函数符号需要由外部动态库提供

┌──(root kali)-[~/print]
└─# nm print_c                  
00000000000002e8 r __abi_tag
0000000000004030 B __bss_start
0000000000004030 b completed.0
                 w __cxa_finalize@GLIBC_2.2.5
0000000000004020 D __data_start
0000000000004020 W data_start
0000000000001080 t deregister_tm_clones
00000000000010f0 t __do_global_dtors_aux
0000000000003df0 d __do_global_dtors_aux_fini_array_entry
0000000000004028 D __dso_handle
0000000000003df8 d _DYNAMIC
0000000000004030 D _edata
0000000000004038 B _end
00000000000011c4 T _fini
0000000000001130 t frame_dummy
0000000000003de8 d __frame_dummy_init_array_entry
000000000000214c r __FRAME_END__
0000000000004000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
000000000000200c r __GNU_EH_FRAME_HDR
0000000000001000 t _init
0000000000003df0 d __init_array_end
0000000000003de8 d __init_array_start
0000000000002000 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
00000000000011c0 T __libc_csu_fini
0000000000001160 T __libc_csu_init
                 U __libc_start_main@GLIBC_2.2.5
0000000000001139 T main
                 U puts@GLIBC_2.2.5
00000000000010b0 t register_tm_clones
0000000000001050 T _start
0000000000004030 D __TMC_END__

其中对应符号是U的都是当前函数为定义的,需要动态引用

而go的(部分)没有一个U,也就是不需要动态引用

┌──(root kali)-[~/print]
└─# nm print_go
00000000004b0f30 r $f64.3eb0000000000000
00000000004b0f38 r $f64.3f50624dd2f1a9fc
00000000004b0f40 r $f64.3f847ae147ae147b
00000000004b0f48 r $f64.3fd0000000000000
00000000004b0f50 r $f64.3fd3333333333333
00000000004b0f58 r $f64.3fe0000000000000
00000000004b0f60 r $f64.3fe3333333333333
00000000004b0f68 r $f64.3fec000000000000
00000000004b0f70 r $f64.3fee666666666666
00000000004b0f78 r $f64.3ff0000000000000
00000000004b0f80 r $f64.3ff199999999999a
00000000004b0f88 r $f64.3ff3333333333333
00000000004b0f90 r $f64.4014000000000000
00000000004b0f98 r $f64.4024000000000000
00000000004b0fa0 r $f64.403a000000000000
00000000004b0fa8 r $f64.4059000000000000
00000000004b0fb0 r $f64.40c3880000000000
00000000004b0fb8 r $f64.40f0000000000000
00000000004b0fc0 r $f64.416312d000000000
00000000004b0fc8 r $f64.43e0000000000000
00000000004b0fd0 r $f64.7ff0000000000000
00000000004b0fd8 r $f64.8000000000000000
00000000004b0fe0 r $f64.bfd3333333333333
00000000004b0fe8 r $f64.bfe62e42fefa39ef
0000000000457d40 t aeshashbody
0000000000457ca0 t callRet
0000000000524e00 B _cgo_callers
0000000000524e08 B _cgo_init
0000000000524e10 B _cgo_mmap
0000000000524e18 B _cgo_munmap
0000000000524e20 B _cgo_notify_runtime_init_done
0000000000524e28 B _cgo_sigaction
0000000000524e30 B _cgo_thread_start
0000000000524e38 B _cgo_yield
0000000000401d40 t cmpbody
00000000004584e0 t debugCall1024
0000000000458360 t debugCall128
00000000004586e0 t debugCall16384
0000000000458560 t debugCall2048
00000000004583e0 t debugCall256
00000000004582a0 t debugCall32
0000000000458760 t debugCall32768
00000000004585e0 t debugCall4096
0000000000458460 t debugCall512
0000000000458300 t debugCall64
00000000004587e0 t debugCall65536
0000000000458660 t debugCall8192
00000000004b1650 r debugCallFrameTooLarge
000000000045e4a0 T errors.(*errorString).Error
0000000000524f50 B errors.errorType
000000000045e4c0 T errors.init
000000000050e560 D errors..inittask
0000000000524f60 B fmt.boolError
0000000000478a80 T fmt.(*buffer).writeRune
0000000000524f70 B fmt.complexError
0000000000476ce0 T fmt.(*fmt).fmtBoolean
0000000000477920 T fmt.(*fmt).fmtBs
00000000004780a0 T fmt.(*fmt).fmtC
0000000000478220 T fmt.(*fmt).fmtFloat
00000000004770c0 T fmt.(*fmt).fmtInteger
0000000000477f40 T fmt.(*fmt).fmtQ
0000000000478140 T fmt.(*fmt).fmtQc
00000000004778a0 T fmt.(*fmt).fmtS
00000000004779a0 T fmt.(*fmt).fmtSbx
0000000000476d60 T fmt.(*fmt).fmtUnicode
0000000000476760 T fmt.(*fmt).pad
0000000000476a20 T fmt.(*fmt).padString
0000000000477760 T fmt.(*fmt).truncate
0000000000477660 T fmt.(*fmt).truncateString

go把所有的运行需要的函数代码都放到了二进制文件中,这就是静态链接,而c中还需要引用外部的函数实体则称为动态链接

那么我们编译的所有go程序都是静态链接的吗?其实并不是,看看下面的代码(短短几行代码就要用四个库,不大才怪)

package main

import (
"fmt"
"io"
"net"
"os"
)


func main() {
conn,err:=net.Dial("tcp","127.0.0.1:3306")
if err!=nil{
fmt.Println(err)
}
io.Copy(os.Stdout,conn)
}

默认编译选项编译查看依赖。

┌──(root kali)-[~/tcpconn]
└─# readelf -a main | grep NEEDED                                                                 
 0x0000000000000001 (NEEDED)             共享库:[libpthread.so.0]
 0x0000000000000001 (NEEDED)             共享库:[libc.so.6]

坏起来了啊,为什么默认的编译选项编译出来的go也会有依赖项,c则在一旁暗自窃喜:以五十步笑百步,到头来不还是需要依赖动态链接库

其实原因在cgo

在上面的环境变量中有这样一个参数

CGO_ENABLED="1"

静态编译和动态编译的开关

当CGO_ENABLED="1"进行编译的时候,会将文件中引用的libc的库(比如常见的net包),以动态连接的方式生成目标文件

当CGO_ENABLED=0, 进行编译时, 则会把在目标文件中未定义的符号(外部函数)一起链接到可执行文件中。

说白了就是这个环境变量为1的时候会动态编译,会引用部分动态链接库,当他为0的时候,就完全静态,我们来测试一下

还是上面同样的代码,这次改一下环境变量

┌──(root kali)-[~/tcpconn]
└─# CGO_ENABLED="0" go build main.go                                                                


┌──(root kali)-[~/tcpconn]
└─# la
.DS_Store  go.mod  main  main.go


┌──(root kali)-[~/tcpconn]
└─# readelf -a main | grep NEEDED   

这下就没有外部依赖变成纯纯的静态编译了

这里先给出个结论(下一章代理会用到):当我们CGO为0的时候,才会纯静态编译不会有其他依赖,当为1的时候使用某些库的时候会有依赖也就是动态编译,再跨平台编译的时候,既是CGO为1也会静态编译,因为找不到对应的动态连接库,所以go本身跨平台编译只支持静态编译,不能动态编译。这里就有一个坑,有一些库必须要使用动态编译(目前见过sqlit3),两种解决办法,一种是到对应平台启用CGO编译,不跨平台编译,还有一种我没试过但是据说可以是利用第三方工具,方法放在这里需要自取:https://zhuanlan.zhihu.com/p/338891206

再来介绍一个CGO是什么

其实本质上,他就是一个能在go中调用c代码的东西,然后我们追溯到我们上面代码用到的库,在这些库中有一些文件开头的注释中有这么一行

// +build cgo

也即是说,这个文件中的方法函数也可以调用c来实现,这样看来当CGO开启的时候,部分代码由c实现,调用了操作系统的动态链接也就对上了吧。禁用CGO的时候就全使用go实现,也就实现了纯静态编译不调用任何依赖

总结

在说一遍,CGO==0静态编译不会调用依赖,CGO==1动态编译可能会调用依赖,跨平台编译不支持CGO,也就是说跨平台编译是静态编译

坑:go写的东西非必要最好静态编译,不然同样的程序在不同的平台跑起来由于掉的依赖不同可能发生问题。例子:zscan扫描之前默认100线程在我mac上跑的和快,我以为就可以了,但是某一天突然发现在其他平台龟速,正常我三分钟能扫完的任务其他平台要二三十分钟,就因为调用的动态链接库不同导致的。

# 信息技术 # 华云安
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录