根据上一章的内容得知,其实不同系统的可执行文件都有自己的格式。只要生成对应的格式后,并且有执行权限就可以执行。

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

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

1.程序入口点

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

1.1 查看程序入口

  其实想要查看程序入口点,有很多工具比如说objdump、readelf、gdb都可以查看,但是为了解析程序入口点,还是选择使用objdump。命令如下:

#objdump -f demo

11.png
<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

12.png
<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进行链接的时候对应的把执行结构链接起来,那么执行结构中其实就包含入口点。

  所以当要知道入口点来源于时,可以去看链接过程。
21.jpeg
<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...


c_rain
23 声望8 粉丝