01 编译

交叉、动态、静态

问题一

默认情况下,golang的编译是动态编译,通过环境变量CGO_ENABLED控制,默认开启cgo,允许在Go代码中调用C代码,如下所示:

go env

...
GCCGO="gccgo"
CC="gcc"
# 就是这个,默认开启
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 \
-fdebug-prefix-map=/tmp/go-build071200252=/tmp/go-build -gno-record-gcc-switches"
...

动态编译完成后的二进制文件,放在Alpine基础镜像中运行报错如下,但是放在ubuntu基础镜像中可以运行:

standard_init_linux.go:211: exec user process caused "no such file or directory"

因为,在制作Alpine的时候,是基于musl libc和busybox构建的,导致动态依赖的二进制文件在运行时找不到依赖的文件。

问题一解决方案

  1. 使用动态编译后运行在大基础镜像中,即包含动态调用的C库的基础镜像
  2. 使用静态编译后运行在小基础镜像中

注意,有的情况下,可能会出现不能静态编译的依赖包,如libpcap这个库。

问题二

使用CGO_ENABLED=1 go build -a -ldflags '-extldflags "-static"' .编译web应用后,无法在scratch和Alpine基础镜像中运行,但是可以直接在开发环境的服务器上运行。

具体表现为,容器启动后直接退出,Exited (127) xxx ago

问题二解决方案

使用CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .,编译后的web应用可以正常运行。

也就是说,动态编译静态链接不行,直接静态编译就可以。所以,静态编译主要用于有内置库的情况下,这样可以是二进制文件在更小的镜像中运行;动态编译静态链接主要用于没有内置库的情况下,将动态依赖的C库一起编译进来,然后将二进制文件放在更小的镜像中运行。

编译参数说明

参数-a,强制重新编译,不利用缓存或已编译好的部分文件,所有包都是最新的代码重新编译和关联

参数-installsuffix,在软件包安装的目录中增加后缀标识,以保持输出与默认版本分开(如果使用 -race 标识,则后缀就会默认设置为 -race 标识,用于区别 race 和普通的版本)

参数-o,指定编译后的可执行文件名称

参数GOOS,用于标识(声明)程序构建环境的目标操作系统。

参数GOARCH,用于标识(声明)程序构建环境的目标计算机架构。

参数GOHOSTOS,用于标识(声明)程序运行环境的目标操作系统。

参数GOHOSTARCH,用于标识(声明)程序运行环境的目标计算机架构。

参数CGO_ENABLED,用于标识(声明)cgo工具是否可用。

存在交叉编译的情况时,cgo 工具是不可用的。在标准go命令的上下文环境中,交叉编译意味着程序构建环境的目标计算架构的标识与程序运行环境的目标计算架构的标识不同,或者程序构建环境的目标操作系统的标识与程序运行环境的目标操作系统的标识不同。

在制作容器的时候,特别是最求镜像足够小的时候,比如下面的使用scratch来构建,这样必然编译环境和运行环境两者之间的计算机架构和运行环境标识不一致。

那么就要进行交叉编译,而交叉编译不支持 cgo,因此这里要禁用掉它,即必须进行静态编译,关闭 cgo 后,在构建过程中会忽略 cgo 并静态链接所有内置依赖库,而开启 cgo 后,方式将转为动态链接,即动态编译。

常见列表:

操作系统GOOSGOARCH
Windows32windows386
Windows64windowsamd64
OSX32darwin386
OSX64darwinamd64
Linux32linux386
Linux64linuxamd64

scratch

明确为空的镜像,尤其适合构建“从零开始”的镜像。

FROM scratch

在构建基础镜像(例如debianbusybox)或超小型镜像(仅包含一个二进制文件以及它所需要的任何内容(例如hello-world))的上下文中,此镜像最有用。

从Docker1.5.0开始,DOckerfile中的FROM scratch将不进行任何操作,不会在生成的镜像中创建一个额外的层。

创建一个基础镜像:从文看出:

  1. 使用Docker保留的最小镜像scratch作为构建容器的启动,使用scratch表示在Dockerfile文件中的下一条指令将会作为生成镜像中的第一个文件系统层。
  2. 虽然在DockerHub中有scratch,但是不能拉取、运行或者将任何其他镜像贴标签为scratch。相反可以在Dockerfile中引用它,如下例子。
FROM scratch
COPY hello /
CMD ["/hello"]

注意 scratch 镜像几乎不包含任何东西,不支持环境变量也没有shell命令。 因此,基于 scratch 的镜像通过 ADD 指令进行添加,以此绕过目录创建。

分析总结

一切要从golang的可移植性(交叉编译)说起。说到一门编程语言可移植性,一般从下面两个方面考量:

  • 语言自身被移植到不同平台的容易程度;
  • 通过这种语言编译出来的应用程序对平台的适应性。
# 查看当前版本支持的操作系统
go tool dist list

得益于golang独立实现了runtime,移植起来相对容易,如下图所示。

image

runtime是支撑程序运行的基础。如C语言的运行时libc是目前主流操作系统上应用最普遍的运行时,通常以动态链接库的形式(比如:/lib/x86_64-linux-gnu/libc.so.6)随着系统一并发布,它的功能大致有如下几个:

  1. 提供基础库函数调用,比如:strncpy(操作字符串);
  2. 封装syscall,它是操作系统提供的API口,当用户层进行系统调用时,代码会trap(陷入)到内核层面执行,并提供同语言的库函数调用,比如:malloc(动态分配内存);
  3. 提供程序启动入口函数,比如:linux下的__libc_start_main

libc等c runtime lib是很早以前就已经实现的了,甚至有些老旧的libc还是单线程的。一些从事c/c++开发多年的程序员早年估计都有过这样的经历:那就是链接runtime库时甚至需要选择链接支持多线程的库还是只支持单线程的库。除此之外,c runtime的版本也参差不齐。这样的c runtime状况完全不能满足golang自身的需求;另外Go的目标之一是原生支持并发,并使用goroutine模型,c runtime对此是无能为力的,因为c runtime本身是基于线程模型的。综合以上因素,golang自己实现了runtime,并封装了syscall,为不同平台上的go user level代码提供封装完成的、统一的go标准库;同时Go runtime实现了对goroutine模型的支持。

独立实现的go runtime层将Go user-level code与OS syscall解耦,把Go porting到一个新平台时,将runtime与新平台的syscall对接即可(当然porting工作不仅仅只有这些);同时,runtime层的实现基本摆脱了Go程序对libc的依赖,这样静态编译的Go程序具有很好的平台适应性。比如:一个compiled for linux amd64的Go程序可以很好的运行于不同linux发行版(centos、ubuntu)下。

静态编译

go程序在编译时会将go runtime一起编译为二进制文件,因此编译完成后的二进制文件比较大。可以使用ldd (print shared object dependencies)nm (list symbols from object files)工具查看文件的外部动态库依赖。

在Docker化的今天,经常需要静态编译一个Go程序,以便放在Docker容器中。即使没有引用其它的第三方包,只是在程序中使用了标准库net,也会发现编译后的程序依赖glibc,这时候需要glibc-static库并且静态链接。

# 编译指令,关键是CGO_ENABLED=0表示关闭
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

ldd main
    不是动态可执行文件

Go将所有运行需要的函数代码都放到了hellogo中,这就是所谓的“静态链接”。但是,并不是所有情况下,Go都能够不依赖外部动态库。

因为有一些库在编写的时候就依赖的外部动态库,使用cgo调用C语言实现的代码,因此在代码中import这些库,最终编译的二进制文件也必要有外部动态依赖,即设置CGO_ENABLE=0将会导致编译报错,报错如下所示。

CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .
github.com/google/gopacket/pcap
../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:30:22: undefined: pcapErrorNotActivated
../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:52:17: undefined: pcapTPtr
../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:64:10: undefined: pcapPkthdr
../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:102:7: undefined: pcapBpfProgram
../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:103:7: undefined: pcapPkthdr
../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:261:33: undefined: pcapErrorActivated
../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:262:33: undefined: pcapWarningPromisc
../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:263:33: undefined: pcapErrorNoSuchDevice
../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:264:33: undefined: pcapErrorDenied
../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:265:33: undefined: pcapErrorNotUp
../../../go/pkg/mod/github.com/google/gopacket@v1.1.17/pcap/pcap.go:265:33: too many errors

动态编译静态链接

编译命令如下:

CGO_ENABLED=1 go build -a -ldflags '-extldflags "-static"' .

在Go中将-ldflagsgo build命令一起使用,以在构建时将动态信息插入二进制文件中,而无需修改源代码。在此标志中,ld代表链接程序,该程序将已编译源代码的不同部分链接到最终二进制文件中。

  • ldflags代表链接器标志。它向底层Go工具链链接器cmd/link传递了一个标志,该标志使得在构建时从命令行更改导入的软件包的值。

来看看go tool link的参数介绍

go tool link
...
 -X definition
    add string value definition of the form importpath.name=value
 -extldflags flags
    pass flags to external linker
...

实现原理:

  1. 将所有生成的.o文件都打到一个.o文件中
  2. 将其交给外部的链接器,比如gcc/clang去做最终链接处理
  3. 参数中传入-ldflags 'extldflags "-static"',那么gcc/clang将会去做静态链接,将.o中undefined的符号都替换为真正的代码

通过-linkmode=external来强制cmd/link采用external linker。

下面的例子将会强制编译为静态链接的二进制文件。

go build -a -ldflags '-extldflags "-static"' .
# sidecar
/tmp/go-link-136417324/000021.o:在函数‘mygetgrouplist’中:
/opt/go/src/os/user/getgrouplist_unix.go:16: 警告: \
Using 'getgrouplist' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-136417324/000020.o:在函数‘mygetgrgid_r’中:
/opt/go/src/os/user/cgo_lookup_unix.go:38: 警告: \
Using 'getgrgid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-136417324/000020.o:在函数‘mygetgrnam_r’中:
/opt/go/src/os/user/cgo_lookup_unix.go:43: 警告: \
Using 'getgrnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-136417324/000020.o:在函数‘mygetpwnam_r’中:
/opt/go/src/os/user/cgo_lookup_unix.go:33: 警告: \
Using 'getpwnam_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-136417324/000020.o:在函数‘mygetpwuid_r’中:
/opt/go/src/os/user/cgo_lookup_unix.go:28: 警告: \
Using 'getpwuid_r' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/tmp/go-link-136417324/000006.o:在函数‘_cgo_26061493d47f_C2func_getaddrinfo’中:
/tmp/go-build/cgo-gcc-prolog:58: 警告: \
Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/libpcap.a(nametoaddr.o):在函数‘pcap_nametoaddr’中:(.text+0x5): 警告: \
Using 'gethostbyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/libpcap.a(nametoaddr.o):在函数‘pcap_nametonetaddr’中:(.text+0xc5): 警告: \
Using 'getnetbyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/libpcap.a(nametoaddr.o):在函数‘pcap_nametoproto’中:(.text+0x305): 警告: \
Using 'getprotobyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
/usr/lib/gcc/x86_64-linux-gnu/7/../../../x86_64-linux-gnu/libpcap.a(nametoaddr.o):在函数‘pcap_nametoport’中:(.text+0xfb): 警告: \
Using 'getservbyname' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking


ldd sidecar
    不是动态可执行文件

动态编译

默认golang设置CGO_ENABLED=1

go build .

ldd sidecar
    linux-vdso.so.1 (0x00007ffda0bb6000)
    libpcap.so.0.8 => /usr/lib/x86_64-linux-gnu/libpcap.so.0.8 (0x00007fcba7fb5000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fcba7d96000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcba79a5000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fcba81f6000)

扩展-Linux分析文件

二进制指每天运行的可执行文件,从命令行工具到成熟的应用程序都是。Linux 提供了一套丰富的工具,让分析二进制文件变得轻而易举。

file

用途:帮助确定文件类型。

二进制文件、库文件、ASCII 文本文件、视频文件、图片文件、PDF、数据文件等各种文件类型

sugoi@sugoi:~/Documents/Golang-Guide$ file /bin/ls \
/bin/ls: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, \
BuildID[sha1]=2f15ad836be3339dec0e2e6a3c637e08e48aacbd, for GNU/Linux 3.2.0, stripped

sugoi@sugoi:~/Documents/Golang-Guide$ file /etc/passwd
/etc/passwd: ASCII text

ldd

用途:打印共享对象依赖关系。

在开发软件的时候,如打印输出或从标准输入/打开的文件中读取等是大多数软件都需要的。所有这些被抽象成一组通用的函数,被放在一个叫 libcglibc 的库中。

对动态链接的二进制文件运行ldd命令会显示出所有依赖库和它们的路径。

sugoi@sugoi:~/Documents/Golang-Guide$ ldd /bin/ls
        linux-vdso.so.1 (0x00007ffd119f3000)
        libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f9ca976e000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f9ca957c000)
        libpcre2-8.so.0 => /usr/lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f9ca94ec000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f9ca94e6000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f9ca97d2000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f9ca94c3000)

ltrace

用途:库调用跟踪器具。

使用 ldd 命令找到可执行程序所依赖的库。然而,一个库可以包含数百个函数。ltrace 命令可以显示运行时从库中调用的所有函数。

如下,可以看到被调用的函数名称,以及传递给该函数的参数,在输出的最右边看到这些函数返回的内容。

$ ltrace ls
__libc_start_main(0x4028c0, 1, 0x7ffd94023b88, 0x412950 <unfinished ...>
strrchr("ls", '/')                                                                  = nil
setlocale(LC_ALL, "")                                                               = "en_US.UTF-8"
bindtextdomain("coreutils", "/usr/share/locale")                                    = "/usr/share/locale"
textdomain("coreutils")                                                             = "coreutils"
__cxa_atexit(0x40a930, 0, 0, 0x736c6974756572)                                      = 0
isatty(1)                                                                           = 1
getenv("QUOTING_STYLE")                                                             = nil
getenv("COLUMNS")                                                                   = nil
ioctl(1, 21523, 0x7ffd94023a50)                                                     = 0
<< snip >>
fflush(0x7ff7baae61c0)                                                              = 0
fclose(0x7ff7baae61c0)                                                              = 0
+++ exited (status 0) +++
$

直接执行,没有这样的输出,很奇怪了

hexdump

用途:以 ASCII、十进制、十六进制或八进制显示文件内容。

$ hexdump -C /bin/ls | head
00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  02 00 3e 00 01 00 00 00  d4 42 40 00 00 00 00 00  |..>......B@.....|
00000020  40 00 00 00 00 00 00 00  f0 c3 01 00 00 00 00 00  |@...............|
00000030  00 00 00 00 40 00 38 00  09 00 40 00 1f 00 1e 00  |....@.8...@.....|
00000040  06 00 00 00 05 00 00 00  40 00 00 00 00 00 00 00  |........@.......|
00000050  40 00 40 00 00 00 00 00  40 00 40 00 00 00 00 00  |@.@.....@.@.....|
00000060  f8 01 00 00 00 00 00 00  f8 01 00 00 00 00 00 00  |................|
00000070  08 00 00 00 00 00 00 00  03 00 00 00 04 00 00 00  |................|
00000080  38 02 00 00 00 00 00 00  38 02 40 00 00 00 00 00  |8.......8.@.....|
00000090  38 02 40 00 00 00 00 00  1c 00 00 00 00 00 00 00  |8.@.............|
$

strings

用途:打印文件中的可打印字符的字符串。

在开发软件的时候,各种文本/ASCII 信息会被添加到其中,比如打印信息、调试信息、帮助信息、错误等。只要这些信息都存在于二进制文件中,就可以用 strings 命令将其转储到屏幕上。

sugoi@sugoi:~/Documents/TUO/tuoctl$ strings /bin/ls | head
/lib64/ld-linux-x86-64.so.2
.j<c~
MB#F-
libselinux.so.1
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
fgetfilecon
freecon
lgetfilecon

readelf

用途:显示有关ELF文件的信息。

上面使用file命令的时候,有输出ELF这种文件类型。

ELF( 可执行和可链接文件格式(Executable and Linkable File Format))是可执行文件或二进制文件的主流格式,不仅是 Linux 系统,也是各种 UNIX 系统的主流文件格式。

在使用 readelf 命令时,有一份实际的 ELF 规范的参考是非常有用的。

sugoi@sugoi:~/Documents/TUO/tuoctl$ readelf -h /bin/ls
ELF 头:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  类别:                              ELF64
  数据:                              2 补码,小端序 (little endian)
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI 版本:                          0
  类型:                              DYN (共享目标文件)
  系统架构:                          Advanced Micro Devices X86-64
  版本:                              0x1
  入口点地址:               0x67d0
  程序头起点:          64 (bytes into file)
  Start of section headers:          140224 (bytes into file)
  标志:             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29

objdump

用途:从对象文件中显示信息。

二进制文件是通过源码创建的,这些源码会通过编译器进行编译。编译器会生成相对于源代码的机器语言指令,然后由 CPU 执行特定的任务。这些机器语言代码可以通过被称为汇编语言的助记词来解读。汇编语言是一组指令,它可以帮助你理解由程序所进行并最终在 CPU 上执行的操作。

objdump 实用程序读取二进制或可执行文件,并将汇编语言指令转储到屏幕上。汇编语言知识对于理解 objdump 命令的输出至关重要。

请记住:汇编语言是特定于体系结构的

sugoi@sugoi:~/Documents/Golang-Guide$ objdump -d /bin/ls | head

/bin/ls:     文件格式 elf64-x86-64


Disassembly of section .init:

0000000000004000 <.init>:
    4000:       f3 0f 1e fa             endbr64
    4004:       48 83 ec 08             sub    $0x8,%rsp
    4008:       48 8b 05 c9 ef 01 00    mov    0x1efc9(%rip),%rax        # 22fd8 <__gmon_start__>

strace

用途:跟踪系统调用和信号。

strace 工具不是追踪调用的库,而是追踪系统调用。系统调用是与内核对接来完成工作的。

举个例子,如果你想把一些东西打印到屏幕上,会使用标准库 libc 中的 printfputs 函数;但是,在底层,最终会有一个名为 write 的系统调用来实际把东西打印到屏幕上。

sugoi@sugoi:~/Documents/Golang-Guide$ strace /bin/ls
execve("/bin/ls", ["/bin/ls"], 0x7ffea0244060 /* 67 vars */) = 0
brk(NULL)                               = 0x561686ceb000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffc8cfa1fb0) = -1 EINVAL (无效的参数)
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (没有那个文件或目录)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=72366, ...}) = 0
mmap(NULL, 72366, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f468d381000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@p\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=163200, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f468d37f000
mmap(NULL, 174600, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f468d354000
mprotect(0x7f468d35a000, 135168, PROT_NONE) = 0
mmap(0x7f468d35a000, 102400, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x6000) = 0x7f468d35a000
mmap(0x7f468d373000, 28672, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1f000) = 0x7f468d373000
mmap(0x7f468d37b000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x26000) = 0x7f468d37b000
mmap(0x7f468d37d000, 6664, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f468d37d000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0cBR\340\305\370\2609W\242\345)q\235A\1"..., 68, 880) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=2029224, ...}) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0cBR\340\305\370\2609W\242\345)q\235A\1"..., 68, 880) = 68
mmap(NULL, 2036952, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f468d162000
mprotect(0x7f468d187000, 1847296, PROT_NONE) = 0
mmap(0x7f468d187000, 1540096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x25000) = 0x7f468d187000
mmap(0x7f468d2ff000, 303104, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0x7f468d2ff000
mmap(0x7f468d34a000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7f468d34a000
mmap(0x7f468d350000, 13528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f468d350000
close(3)                                = 0
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libpcre2-8.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\340\"\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=584392, ...}) = 0
mmap(NULL, 586536, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f468d0d2000
mmap(0x7f468d0d4000, 409600, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x2000) = 0x7f468d0d4000
mmap(0x7f468d138000, 163840, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x66000) = 0x7f468d138000
mmap(0x7f468d160000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x8d000) = 0x7f468d160000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 \22\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=18816, ...}) = 0
mmap(NULL, 20752, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f468d0cc000
mmap(0x7f468d0cd000, 8192, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1000) = 0x7f468d0cd000
mmap(0x7f468d0cf000, 4096, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f468d0cf000
mmap(0x7f468d0d0000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x3000) = 0x7f468d0d0000
close(3)                                = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220\201\0\0\0\0\0\0"..., 832) = 832
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0w\\\273\377\370\24Ef`xg\200\260\263\264\0"..., 68, 824) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=157224, ...}) = 0
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0w\\\273\377\370\24Ef`xg\200\260\263\264\0"..., 68, 824) = 68
mmap(NULL, 140408, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f468d0a9000
mmap(0x7f468d0b0000, 69632, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x7000) = 0x7f468d0b0000
mmap(0x7f468d0c1000, 20480, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18000) = 0x7f468d0c1000
mmap(0x7f468d0c6000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c000) = 0x7f468d0c6000
mmap(0x7f468d0c8000, 13432, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f468d0c8000
close(3)                                = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f468d0a7000
arch_prctl(ARCH_SET_FS, 0x7f468d0a8400) = 0
mprotect(0x7f468d34a000, 12288, PROT_READ) = 0
mprotect(0x7f468d0c6000, 4096, PROT_READ) = 0
mprotect(0x7f468d0d0000, 4096, PROT_READ) = 0
mprotect(0x7f468d160000, 4096, PROT_READ) = 0
mprotect(0x7f468d37b000, 4096, PROT_READ) = 0
mprotect(0x561684dff000, 4096, PROT_READ) = 0
mprotect(0x7f468d3c0000, 4096, PROT_READ) = 0
munmap(0x7f468d381000, 72366)           = 0
set_tid_address(0x7f468d0a86d0)         = 66950
set_robust_list(0x7f468d0a86e0, 24)     = 0
rt_sigaction(SIGRTMIN, {sa_handler=0x7f468d0b0bf0, sa_mask=[], sa_flags=SA_RESTORER|SA_SIGINFO, sa_restorer=0x7f468d0be3c0}, NULL, 8) = 0
rt_sigaction(SIGRT_1, {sa_handler=0x7f468d0b0c90, sa_mask=[], sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO, sa_restorer=0x7f468d0be3c0}, NULL, 8) = 0
rt_sigprocmask(SIG_UNBLOCK, [RTMIN RT_1], NULL, 8) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
statfs("/sys/fs/selinux", 0x7ffc8cfa1f00) = -1 ENOENT (没有那个文件或目录)
statfs("/selinux", 0x7ffc8cfa1f00)      = -1 ENOENT (没有那个文件或目录)
brk(NULL)                               = 0x561686ceb000
brk(0x561686d0c000)                     = 0x561686d0c000
openat(AT_FDCWD, "/proc/filesystems", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
read(3, "nodev\tsysfs\nnodev\ttmpfs\nnodev\tbd"..., 1024) = 385
read(3, "", 1024)                       = 0
close(3)                                = 0
access("/etc/selinux/config", F_OK)     = -1 ENOENT (没有那个文件或目录)
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=8850624, ...}) = 0
mmap(NULL, 8850624, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f468c836000
close(3)                                = 0
ioctl(1, TCGETS, {B38400 opost isig icanon echo ...}) = 0
ioctl(1, TIOCGWINSZ, {ws_row=16, ws_col=186, ws_xpixel=0, ws_ypixel=0}) = 0
openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 3
fstat(3, {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
getdents64(3, /* 12 entries */, 32768)  = 360
getdents64(3, /* 0 entries */, 32768)   = 0
close(3)                                = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1), ...}) = 0
write(1, "archetypes  config.yaml  content"..., 80archetypes  config.yaml  content  LICENSE  README.md  resources  static  themes
) = 80
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

nm

用途:列出对象文件中的符号。

如果使用的二进制文件没有被剥离,nm 命令将提供在编译过程中嵌入到二进制文件中的有价值的信息。nm 可以从二进制文件中识别变量和函数。在无法访问二进制文件的源代码时这很有用。

$ cat hello.c
#include <stdio.h>

int main() {
    printf("Hello world!");
    return 0;
}

$ gcc -g hello.c -o hello

$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), \
dynamically linked (uses shared libs), for GNU/Linux 2.6.32, \
BuildID[sha1]=3de46c8efb98bce4ad525d3328121568ba3d8a5d, not stripped

$ ./hello
Hello world!


$ nm hello | tail
0000000000600e20 d __JCR_END__
0000000000600e20 d __JCR_LIST__
00000000004005b0 T __libc_csu_fini
0000000000400540 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
000000000040051d T main
                 U printf@@GLIBC_2.2.5
0000000000400490 t register_tm_clones
0000000000400430 T _start
0000000000601030 D __TMC_END__
$

gdb

用途:GNU调试器。

不是所有的二进制文件中的东西都可以进行静态分析。唯一方法是在运行时环境,在任何给定的位置停止或暂停程序,并能够分析信息,然后再往下执行。

这就是调试器的作用,在 Linux 上,gdb 就是调试器的事实标准。它可以加载程序,在特定的地方设置断点,分析内存和 CPU 的寄存器,以及更多的功能。它是对上面工具的补充,可以做更多的运行时分析。

使用 gdb 加载一个程序,会看到 (gdb) 提示符。所有进一步的命令都将在这个 gdb 命令提示符中运行,直到退出。

$ gdb -q ./hello
Reading symbols from /home/flash/hello...done.
(gdb) break main
Breakpoint 1 at 0x400521: file hello.c, line 4.
(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000400521 in main at hello.c:4
(gdb) run
Starting program: /home/flash/./hello

Breakpoint 1, main () at hello.c:4
4           printf("Hello world!");
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7_6.6.x86_64
(gdb) bt
#0  main () at hello.c:4
(gdb) c
Continuing.
Hello world![Inferior 1 (process 29620) exited normally]
(gdb) q
$
上次修改: 14 March 2020