现在自己用的最多的编程语言是golang,golang是编译型语言,需要先把程序编译为二进制,然后启动执行。之前想当然认为golang编译的二进制有它特殊的地方,需要特殊流程才可以启动。实际各个二进制可执行文件是平等的个体,它们遵循相同的启动流程。
下面介绍linux系统上,二进制可执行文件如何启动执行。
使用strace可显示二进制可执行文件启动时的系统调用。下面执行trace sleep1000s观察sleep二进制文件如何执行的。
➜ ~ strace sleep 1000s execve("/usr/bin/sleep", ["sleep", "1000s"], 0x7ffc9b87ff18 /* 68 vars */) = 0brk(NULL) = 0x55b94789c000arch_prctl(0x3001 /* ARCH_??? */, 0x7fffcfee0760) = -1 EINVAL (Invalid argument)access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3fstat(3, {st_mode=S_IFREG|0644, st_size=120038, ...}) = 0mmap(NULL, 120038, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f403c9d4000close(3) = 0openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3。。。省略部分无关trace。。。
由上面示例可知是execve系统调用启动了sleep,execve系统调用有何作用:把当前执行程序的堆栈及数据替换为新的程序,并执行新的程序,但是程序号保持不变。把当前执行程序替换为新的程序并执行如何理解?我们能否观察到此过程?
我们可以追踪一个终端进程如何执行sleep 1000s子程序,来理解execve系统调用如何把当前执行程序替换为新的程序。在下面trace日志中,我们追踪进程301345,它是一个终端的进程 id,在此终端运行sleep 1000s,strace会记录终端clone出子进程302078,然后execve系统调用把子进程执行程序替换为sleep。
///在终端(进程id 301345) 执行 sleep 1000s➜ ~ sleep 1000s
# strace -f -e trace=all -p 301345。。。省略部分无关trace。。301345 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f358399d710) = 302078301345 close(4) = 0301345 read(3, <unfinished ...>302078 close(3) = 0302078 getpid() = 302078302078 setpgid(0, 302078) = 0302078 ioctl(10, TIOCSPGRP, [302078]) = 0 。。。省略部分无关trace。。302078 execve("/usr/bin/sleep", ["sleep", "1000s"], 0x557bdd6785c0 /* 67 vars */) = 0 302078 brk(NULL) = 0x5592d0454000302078 arch_prctl(0x3001 /* ARCH_??? */, 0x7ffc8bfbe550) = -1 EINVAL (Invalid argument)302078 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)302078 openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3。。。省略部分无关trace。。
另外,可以使用pstree 查看进程树,进一步理解sleep 的启动流程。终端zsh(301345) clone出子进程302078,然后系统调用execve把子进程执行程序替换为sleep,且进程id不变。
➜ ~ pstree -lphs 302078systemd(1)───systemd(300022)───gnome-shell(300318)───terminator(301334)───zsh(301345)───sleep(302078)
可以写一个C语言程序,创建一个子进程,然后系统调用execve把子进程执行程序替换为sleep,如下所示bin.c 文件内容如下。
#include <stdio.h>#include <stdlib.h>#include <unistd.h>int main() { pid_t pid = fork(); // 创建子进程 if (pid == -1) { // fork失败 perror("fork failed"); exit(EXIT_FAILURE); } else if (pid == 0) { // 子进程 execl("/user/bin/sleep", "sleep", "1000s", NULL); // 替换子进程执行程序为'sleep 1000s'命令 perror("execl failed"); exit(EXIT_FAILURE); } else { // 父进程 int status; waitpid(pid, &status, 0); // 等待子进程结束 if (WIFEXITED(status)) { printf("子进程退出,返回值为: %d\n", WEXITSTATUS(status)); } } return 0;}
编译此C语言文件,并通过strace追踪它执行时的系统调用。可见,fork 创建子进程会使用clone 系统调用, execl改变子进程执行程序时,会使用execve系统调用,最终使子进程运行sleep。感觉我们可以用C语言开发一个终端程序。
➜ ~ gcc bin.c -o bin➜ ~ strace -f ./bin。。。省略部分无关trace。。306145 munmap(0x7f7d058ab000, 120038) = 0306145 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7d058aa810) = 306146306146 execve("/usr/bin/sleep", ["sleep", "1000s"], 0x7ffebc541708 /* 67 vars */ <unfinished ...>306145 wait4(306146, <unfinished ...>。。。省略部分无关trace。。➜ ~ pstree -lps 306146 systemd(1)───systemd(300022)───gnome-shell(300318)───terminator(301334)───zsh(302463)───strace(306141)───bin(306145)───sleep(306146)
golang语言编译的二进制可执行文件,启动流程和上面的示例启动流程一致,因为各个二进制可执行文件是平等的个体,在程序世界它们遵循相同的启动流程。
我们来写一个简单golang hello world程序验证下,hello.go如下所示
package main import "fmt" func main() { fmt.Println("Hello, World!") }
编译hello.go为二进制可执行文件。
➜ ~ go mod init example.com/hellogo: creating new go.mod: module example.com/hellogo: to add module requirements and sums: go mod tidy➜ ~ go build➜ ~ ./hello
在终端执行它,使用strace追踪此二进制文件如何启动,也是先clone出子进程317645,然后系统调用execve把子进程执行程序替换为hello
/// 使用strace追踪 终端如何启动golang 二进制文件➜ ~ strace -f -e trace=all -p 313682。。。省略部分无关trace。。313682 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD <unfinished ...>317645 close(3) = 0313682 <... clone resumed>, child_tidptr=0x7f7ff76da710) = 317645317645 getpid( <unfinished ...>313682 close(4 <unfinished ...>317645 <... getpid resumed>) = 317645317645 setpgid(0, 317645 <unfinished ...>。。。省略部分无关trace。。317645 execve("./hello", ["./hello"], 0x55705c7de5c0 /* 67 vars */) = 0317645 arch_prctl(ARCH_SET_FS, 0x52cb50) = 0。。。省略部分无关trace。。
本文由思考golang编译的二进制可执行文件如何启动,学习了linux系统下,二进制可执行文件的启动流程。经过调研知道,在程序世界,各个二进制可执行文件遵循相同的启动流程,它们是平等个体。