默认情况下,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构建的,导致动态依赖的二进制文件在运行时找不到依赖的文件。
注意,有的情况下,可能会出现不能静态编译的依赖包,如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 后,方式将转为动态链接,即动态编译。
常见列表:
操作系统 | GOOS | GOARCH |
---|---|---|
Windows32 | windows | 386 |
Windows64 | windows | amd64 |
OSX32 | darwin | 386 |
OSX64 | darwin | amd64 |
Linux32 | linux | 386 |
Linux64 | linux | amd64 |
明确为空的镜像,尤其适合构建“从零开始”的镜像。
FROM scratch
在构建基础镜像(例如debian和busybox)或超小型镜像(仅包含一个二进制文件以及它所需要的任何内容(例如hello-world))的上下文中,此镜像最有用。
从Docker1.5.0开始,DOckerfile中的FROM scratch
将不进行任何操作,不会在生成的镜像中创建一个额外的层。
创建一个基础镜像:从文看出:
scratch
作为构建容器的启动,使用scratch
表示在Dockerfile文件中的下一条指令将会作为生成镜像中的第一个文件系统层。scratch
,但是不能拉取、运行或者将任何其他镜像贴标签为scratch
。相反可以在Dockerfile中引用它,如下例子。FROM scratch COPY hello / CMD ["/hello"]
注意 scratch 镜像几乎不包含任何东西,不支持环境变量,也没有shell命令。 因此,基于 scratch 的镜像通过 ADD 指令进行添加,以此绕过目录创建。
一切要从golang的可移植性(交叉编译)说起。说到一门编程语言可移植性,一般从下面两个方面考量:
# 查看当前版本支持的操作系统 go tool dist list
得益于golang独立实现了runtime,移植起来相对容易,如下图所示。
runtime是支撑程序运行的基础。如C语言的运行时libc
是目前主流操作系统上应用最普遍的运行时,通常以动态链接库的形式(比如:/lib/x86_64-linux-gnu/libc.so.6
)随着系统一并发布,它的功能大致有如下几个:
strncpy
(操作字符串);syscall
,它是操作系统提供的API口,当用户层进行系统调用时,代码会trap(陷入)到内核层面执行,并提供同语言的库函数调用,比如:malloc
(动态分配内存);__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中将
-ldflags
与go 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 ...
实现原理:
.o
文件都打到一个.o
文件中gcc/clang
去做最终链接处理-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 提供了一套丰富的工具,让分析二进制文件变得轻而易举。
用途:帮助确定文件类型。
二进制文件、库文件、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
用途:打印共享对象依赖关系。
在开发软件的时候,如打印输出或从标准输入/打开的文件中读取等是大多数软件都需要的。所有这些被抽象成一组通用的函数,被放在一个叫
libc
或glibc
的库中。
对动态链接的二进制文件运行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)
用途:库调用跟踪器具。
使用
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) +++ $
直接执行,没有这样的输出,很奇怪了。
用途:以 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.@.............| $
用途:打印文件中的可打印字符的字符串。
在开发软件的时候,各种文本/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
用途:显示有关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
用途:从对象文件中显示信息。
二进制文件是通过源码创建的,这些源码会通过编译器进行编译。编译器会生成相对于源代码的机器语言指令,然后由 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 工具不是追踪调用的库,而是追踪系统调用。系统调用是与内核对接来完成工作的。
举个例子,如果你想把一些东西打印到屏幕上,会使用标准库 libc
中的 printf
或 puts
函数;但是,在底层,最终会有一个名为 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
可以从二进制文件中识别变量和函数。在无法访问二进制文件的源代码时这很有用。
$ 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__ $
用途: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 $