共计 4093 个字符,预计需要花费 11 分钟才能阅读完成。
依据上一章的内容得悉,其实不同零碎的可执行文件都有本人的格局。只有生成对应的格局后,并且有执行权限就能够执行。
那么问题来了,所说的程序入口点到底是什么?可编译性语言,不同的语言的入口点不一样,大多数的都叫 main。那么不能叫其余的吗?main 真的是入口点吗?如同有很多问题须要摸索,须要去开掘。
既然这么多问题,就带着问题来看看 go1.3 的入口点是什么?
1. 程序入口点
说到程序入口点,这个其实很容易了解,就是程序启动的开始地址。那么接着之前的文章中生成的 demo 程序,来看看程序入口点。
1.1 查看程序入口
其实想要查看程序入口点,有很多工具比如说 objdump、readelf、gdb 都能够查看,然而为了解析程序入口点,还是抉择应用 objdump。命令如下:
#objdump -f demo
<center> 图 1 -1 查看程序信息 </center>
demo: file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x0000000000421790
通过命令查看后,能够看到与文件的格局类型是 elf64-x86-64、并且程序的入口地址是 0x0000000000421790, 也就是“start address 0x0000000000421790”这段形容,如图 1 - 1 所示。
1.2 追踪程序入口
通过 1.1 大节中得悉程序入口地址为 0x0000000000421790, 那么能够持续应用 objdump 命令持续追踪下程序入口。命令如下:
#objdump --disassemble demo |grep 421790 -C 10
<center> 图 1 -2 追踪程序入口 </center>
通过命令 objdump 查看入口点汇编代码,过滤掉查看前后 10 行。如图 1 - 2 所示,程序入口是_rt0_amd64_linux,并不是 main。
1.3 大节
从追踪内容来看,其实程序入口并不是想的那样肯定是 main,也能够变更为其余函数。
其实有聪慧的人就会想,那么批改入口点到本人的内存地址做肯定的操作在跳转到入口点做到神不知鬼不觉。其实你说到对,这个就是所谓对 hook api 技术,也就是函数劫持。
不过利用级编程劫持地址并不是想的那样,如果你要从 A 程序去劫持到 B 程序是做不到的,因为应用层的程序间都是虚拟地址变更是做不到的。然而也不是齐全做不到,A 程序能够往 B 程序注入动态链接库,通过动态链接库则能够操作内存。内核级劫持则没有那么简单, 不过操作不当容易蓝屏。
2. 解析程序入口源码
曾经晓得入口是_rt0_amd64_linux,那么来看看到底_rt0_amd64_linux 是怎么来的。
2.1 追踪_rt0_amd64_linux
在晓得入口是_rt0_amd64_linux 之后,其实能够思考入口必定是编译阶段编译进去的,然而通过上一张得悉 6l 进行链接的时候对应的把执行构造链接起来,那么执行构造中其实就蕴含入口点。
所以当要晓得入口点来源于时,能够去看链接过程。
<center> 图 2 -1 追踪_rt0_amd64_linux </center>
通过图 2 - 1 能够得悉,在 6l 编译时初始化了入口点,并且生成程序,不同的零碎入口点是不一样。go 对应的 main 函数,其实是 main.main。main.main 之前的其实都是一些 runtime 的初始化操作,比如说栈大小、内存清理、g0 初始化等等。
留神的是 runtime.main 其实也是一个协程,也就是说 main.main 也是在协程中运行的。
2.2 源码解析
依据图 2 - 1 来解析一下对应 go1.3 链接过程与运行过程中的源代码。
2.2.1 libinit 函数
libinit 函数在 src/cmd/ld/lib.c 中,libinit 函数其实是链接程序时,写入程序入口点。代码如下:
void
libinit(void)
{
char *suffix, *suffixsep;
funcalign = FuncAlign;
fmtinstall('i', iconv);
fmtinstall('Y', Yconv);
fmtinstall('Z', Zconv);
mywhatsys(); // 取得 goroot, goarch, goos。别离代表 go 的 root 目录、go 的运行零碎环境、go 的运行零碎
// add goroot to the end of the libdir list.
suffix = "";
suffixsep = "";
if(flag_installsuffix != nil) {
suffixsep = "_";
suffix = flag_installsuffix;
} else if(flag_race) {
suffixsep = "_";
suffix = "race";
}
Lflag(smprint("%s/pkg/%s_%s%s%s", goroot, goos, goarch, suffixsep, suffix));
mayberemoveoutfile(); // 如果输入文件存储则删除
cout = create(outfile, 1, 0775); // 创立输入文件
if(cout < 0) {diag("cannot create %s: %r", outfile);
errorexit();}
if(INITENTRY == nil) {INITENTRY = mal(strlen(goarch)+strlen(goos)+20);
if(!flag_shared) {sprint(INITENTRY, "_rt0_%s_%s", goarch, goos); // 如果不是动态链接库、则设置入口点
} else {sprint(INITENTRY, "_rt0_%s_%s_lib", goarch, goos); // 如果是动态链接库,则设置动态链接库入口点
}
}
linklookup(ctxt, INITENTRY, 0)->type = SXREF;
}
2.2.2 main 与_rt0_amd64_linux
依据链接办法 libinit 得悉、goarch 参数等于_rt0_amd64_linux、goos 参数等于 linux,因为应用的是动态编译, 则得出 INITENTRY 等于_rt0_amd64_linux,也就是对应的程序入口点。
_rt0_amd64_linux 对应的办法文件为 src/pkg/runtime/rt0_linux_amd64.s,是通过汇编模式编写,如下:
#include "../../cmd/ld/textflag.h"
TEXT _rt0_amd64_linux(SB),NOSPLIT,$-8
LEAQ 8(SP), SI // argv
MOVQ 0(SP), DI // argc
MOVQ $main(SB), AX // 设置 main 函数地址给 AX 变量
JMP AX // 跳转到 main 函数
TEXT main(SB),NOSPLIT,$-8 //main 函数
MOVQ $_rt0_go(SB), AX // 设置_rt0_go 函数给 AX 变量
JMP AX // 跳转到_rt0_go 函数
依据 rt0_linux_amd64.s 函数得悉、入口点_rt0_amd64_linux 会跳转到 main 函数、main 函数会跳转到_rt0_go 函数。
2.2.3 大节
_rt0_go 函数以及 runtime.main 函数,就不做太多赘述。能够自行去调试验证,不过_rt0_go、_rt0_amd64_linux、main 三个函数是在主线程中、并不是协程程序。调试时还需注意,而后 runtime.main,main.main 是在协程中,是在_rt0_go 中退出 runtime.main,并调用 runtime·mstart 函数启动 M 进行运行调度。
其实除了 go 以外,一些其余语言程序也有本人的入口和 runtime 库,不过大同小异。例如 c 来说,则必须有 libc。能够通过如下命令查看对应 so:
ldconfig -p |grep libc.so
3. 内核调用
晓得入口是怎么回事之后,还想晓得晓得 ELF 是如果在零碎内调度起来的,就能够钻研下内核级源码。
用户空间 ELF 文件加载 函数调用栈(/fs/exec.c):
sys_execve / sys_execveat
└→ do_execve
└→ do_execveat_common
└→ __do_execve_file
└→ exec_binprm
└→ search_binary_handler
└→ load_elf_binary
在函数 search_binary_handler() 中,遍历零碎注册的 binary formats handler 链表,直到找到匹配的格局。elf 文件的处理函数就是 load_elf_binary()。
如果想对内核级源码进行调试,在 linux 中能够应用 systemtap,装置后还须要装置内核版本源码。这样就能够应用 ”stap -g” 命令进行调试内核源码。
能够参考之前写的一篇文章《排查 API 的 connection reset by peer 和 Timeout exceeded 问题》, 有内核调试内容。
总结
通过程序入口点的理解、也晓得如何查看入口点。并且得悉 go 入口的由来,并且把握来一些调试内核技巧。
- objdump、readelf、gdb 能够查看程序入口点。
- 64 位 linux 下,go1.3 程序入口点位_rt0_amd64_linux。
- 6l 命令中的 libinit 函数,用于链接 go 程序入口点。
- main.main 函数为 go 源代码中的 func main 函数。
- systemtap 调试内核源码前,须要装置内核源码包。
- “stap -g”命令可用于调试内核源码。
文章奉献
crt0:
https://en.wikipedia.org/wiki…
ELF 文件可执行栈的深入分析:
https://mudongliang.github.io…
如何在 Linux 上执行 main:
https://web.archive.org/web/2…