热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【Golang源码分析】Golang如何实现自举程序入口点(五)

【Golang源码分析】Golang如何实现自举-程序入口点(五)-  根据上一章的内容得知,其实不同系统的可执行文件都有自己的格式。只要生成对应的格式后,并且有执行权限就可以执行

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

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

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

1.程序入口点

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

1.1 查看程序入口

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

#objdump -f demo


图1-1 查看程序信息

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


图1-2 追踪程序入口

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

  所以当要知道入口点来源于时,可以去看链接过程。

图2-1 追踪_rt0_amd64_linux

  通过图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...


推荐阅读
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 通过Anaconda安装tensorflow,并安装运行spyder编译器的完整教程
    本文提供了一个完整的教程,介绍了如何通过Anaconda安装tensorflow,并安装运行spyder编译器。文章详细介绍了安装Anaconda、创建tensorflow环境、安装GPU版本tensorflow、安装和运行Spyder编译器以及安装OpenCV等步骤。该教程适用于Windows 8操作系统,并提供了相关的网址供参考。通过本教程,读者可以轻松地安装和配置tensorflow环境,以及运行spyder编译器进行开发。 ... [详细]
  • 本文介绍了Python高级网络编程及TCP/IP协议簇的OSI七层模型。首先简单介绍了七层模型的各层及其封装解封装过程。然后讨论了程序开发中涉及到的网络通信内容,主要包括TCP协议、UDP协议和IPV4协议。最后还介绍了socket编程、聊天socket实现、远程执行命令、上传文件、socketserver及其源码分析等相关内容。 ... [详细]
  • 本文介绍了计算机网络的定义和通信流程,包括客户端编译文件、二进制转换、三层路由设备等。同时,还介绍了计算机网络中常用的关键词,如MAC地址和IP地址。 ... [详细]
  • 本文探讨了C语言中指针的应用与价值,指针在C语言中具有灵活性和可变性,通过指针可以操作系统内存和控制外部I/O端口。文章介绍了指针变量和指针的指向变量的含义和用法,以及判断变量数据类型和指向变量或成员变量的类型的方法。还讨论了指针访问数组元素和下标法数组元素的等价关系,以及指针作为函数参数可以改变主调函数变量的值的特点。此外,文章还提到了指针在动态存储分配、链表创建和相关操作中的应用,以及类成员指针与外部变量的区分方法。通过本文的阐述,读者可以更好地理解和应用C语言中的指针。 ... [详细]
  • 本文介绍了在Windows环境下如何配置php+apache环境,包括下载php7和apache2.4、安装vc2015运行时环境、启动php7和apache2.4等步骤。希望对需要搭建php7环境的读者有一定的参考价值。摘要长度为169字。 ... [详细]
  • Linux如何安装Mongodb的详细步骤和注意事项
    本文介绍了Linux如何安装Mongodb的详细步骤和注意事项,同时介绍了Mongodb的特点和优势。Mongodb是一个开源的数据库,适用于各种规模的企业和各类应用程序。它具有灵活的数据模式和高性能的数据读写操作,能够提高企业的敏捷性和可扩展性。文章还提供了Mongodb的下载安装包地址。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • CEPH LIO iSCSI Gateway及其使用参考文档
    本文介绍了CEPH LIO iSCSI Gateway以及使用该网关的参考文档,包括Ceph Block Device、CEPH ISCSI GATEWAY、USING AN ISCSI GATEWAY等。同时提供了多个参考链接,详细介绍了CEPH LIO iSCSI Gateway的配置和使用方法。 ... [详细]
  • 本文概述了JNI的原理以及常用方法。JNI提供了一种Java字节码调用C/C++的解决方案,但引用类型不能直接在Native层使用,需要进行类型转化。多维数组(包括二维数组)都是引用类型,需要使用jobjectArray类型来存取其值。此外,由于Java支持函数重载,根据函数名无法找到对应的JNI函数,因此介绍了JNI函数签名信息的解决方案。 ... [详细]
  • PHP组合工具以及开发所需的工具
    本文介绍了PHP开发中常用的组合工具和开发所需的工具。对于数据分析软件,包括Excel、hihidata、SPSS、SAS、MARLAB、Eview以及各种BI与报表工具等。同时还介绍了PHP开发所需的PHP MySQL Apache集成环境,包括推荐的AppServ等版本。 ... [详细]
  • x86 linux的进程调度,x86体系结构下Linux2.6.26的进程调度和切换
    进程调度相关数据结构task_structtask_struct是进程在内核中对应的数据结构,它标识了进程的状态等各项信息。其中有一项thread_struct结构的 ... [详细]
  • css div中文字位置_超赞的 CSS 阴影技巧与细节
    本文的题目是CSS阴影技巧与细节。CSS阴影,却不一定是box-shadow与filter:drop-shadow,为啥?因为使用其他属性 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 折腾个半死,数据库初始化设置不当报错 ORA01078: failure in proces...
    2019独角兽企业重金招聘Python工程师标准[oraclelocalhost~]$sqlplusassysdba提示Connectedtoanidleinstance.连 ... [详细]
author-avatar
于华521_811
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有