依据上一章的内容得悉,其实不同零碎的可执行文件都有本人的格局。只有生成对应的格局后,并且有执行权限就能够执行。

  那么问题来了,所说的程序入口点到底是什么?可编译性语言,不同的语言的入口点不一样,大多数的都叫main。那么不能叫其余的吗?main真的是入口点吗?如同有很多问题须要摸索,须要去开掘。

  既然这么多问题,就带着问题来看看go1.3的入口点是什么?

1.程序入口点

  说到程序入口点,这个其实很容易了解,就是程序启动的开始地址。那么接着之前的文章中生成的demo程序,来看看程序入口点。

1.1 查看程序入口

  其实想要查看程序入口点,有很多工具比如说objdump、readelf、gdb都能够查看,然而为了解析程序入口点,还是抉择应用objdump。命令如下:

#objdump -f demo


<center>图1-1 查看程序信息 </center>

demo:     file format elf64-x86-64architecture: i386:x86-64, flags 0x00000112:EXEC_P, HAS_SYMS, D_PAGEDstart 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函数其实是链接程序时,写入程序入口点。代码如下:

voidlibinit(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...