关于gcc设置入口函数的讨论

发布时间 2023-04-08 20:12:44作者: zwlwf

关于gcc设置入口函数的讨论

一般的程序入口函数是_start(不是main,参考【2】)。

如果我们想在main之前做点啥工作,或者希望链接一个项目的main.o对象,就需要另外指定入口函数。

虽然gcc提供了指定入口函数的参数,但我发现往往不是我们想要的。

gcc的指定入口函数参数

gcc提供了两个命令行参数,-e funcName, -entry=funcName来指定入口函数。后者已过时,推荐使用前者。这个参数是告诉链接器设置函数的入口,更具体的是在生成的elf文件,头指定Entry point address,即Elf32_Ehdr/Elf64_Ehdr结构体的e_entry项。

在【1】中,直接将入口函数指向了fun,会少了很多预处理工作。特别是C++的global constructor没有初始化,直接指定入口函数基本是达不到我们预期目的的。

观察参考【2】,可以想到修改start.s,给__libc_start_main指定新的“main”地址的手段。

若我想将main函数变成我的testmain怎么办?可以自己写一下start.S,将其中的main变成testmain。

重写start.S调用我们指定的函数接口

gnu之所以采用汇编来写start.S主要是做一些精细化的控制,如rbp置零,rsp中地址末4位置0。

下面是参考Scrt1.o的反汇编写的start.S的汇编(初始版)。

.text
.global _start
.type _start,@function
_start:
  xor %rbp, %rbp
  movq %rdx, %r9
  pop %rsi
  movq %rsp, %rdx
  and $0xfffffffffffffff0, %rsp
  pushq %rax
  pushq %rsp
  xor %r8d, %r8d
  xor %ecx, %ecx
  leaq fun@PLT(%rip), %rdi
  callq __libc_start_main@PLT
  hlt
.size _start,.-_start

这个start.S就可以链接到fun函数。

//main.c
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <stdlib.h>

int main(int, char**);

void render_argmuments(int argc, char**argv, int *arg_num, char*** arg_value) {
  bool hasDelimiter = false;
  for(int i=1; i<argc; i++) {
    if(strcmp(argv[i], "-F")==0) hasDelimiter = true;
  }
  if(hasDelimiter) {
    *arg_num = argc;
    *arg_value = argv;
  } else {
    *arg_num = argc+2;
    *arg_value = (char**)malloc(*arg_num*sizeof(char*));
    char** tmp = *arg_value;
    tmp[0] = argv[0];
    tmp[1] = "-F";
    tmp[2] = ",";
    for(int i=1; i<argc; i++) tmp[i+2] = argv[i];
  }
}

int fun(int argc, char**argv) {
  int arg_num;
  char** arg_value;
  render_argmuments(argc, argv, &arg_num, &arg_value);
  printf("check args:\n");
  for(int i=0; i<arg_num; i++) printf("arg[%d] = %s\n",i,arg_value[i]);
  return main(arg_num, arg_value);
}

这里以使用start.s改入口函数的方式来实现类似gcc 链接时打桩 中默认使用逗号作为分割符的awk。

app : main.c start.S
        cd ~/software/awk/ && gcc -g -Wall -pedantic -Wcast-qual  -O2 ${PWD}/main.c ${PWD}/start.S -nostartfiles awkgram.tab.o b.o main.o parse.o proctab.o tran.o lib.o run.o lex.o  -lm -o ${PWD}/app -fPIC -g

运行下面的命令,同样可以达到预期效果。

echo 1,2,3 | ./app '{print $1,$2}'

关于start.S中汇编指令的复盘

我写的start.S和系统的Sctrl.o还是有点区别的,细究一下,在最后将fun的函数的地址填入%rdi,和调用libc.so中的__libc_start_main函数的两条指令。

第一条指令,采用相对rip的方式将fun@plt地址填入rdi。这里之所以用相对rip的方式,是因为本地的gcc编译程序默认加pie,exe的加载地址非0且不固定。第二条直接调用__libc_start_main@PLT ,注意call指令中,填入的就是函数相对rip的地址。

leaq fun@PLT(%rip), %rdi 
callq __libc_start_main@PLT 

第二条指令里@PLT就告诉了编译器需要通过plt+got表的方式来调用外部符号。当使用gcc时,我们知道这个机制是通过fPIC参数产生的,现在看着个参数作用于.c到.s的编译阶段,直接写start.S这个汇编fPIC就不会起什么作用了。

假设我们需要引用一个外部符号(来自so)。对于x86的汇编,我们可以有如下写法,

callq printf  //直接使用printf的地址-rip来填入。编译器会告警,因为我们需要运行时重定位这条指令,但代码段是只读的!
callq printf@PLT(%rip) //错误,call都是相对的,所以不需要(%rip),汇编器会报一个警示, Warning: indirect call without `*'
callq printf@PLT // 正确,以相对的方式去调用printf@PLT,之所以有printf@plt是为了延迟加载
callq *printf@GOTPCREL(%rip) // 从printf填入got表项的内存取出printf的地址。

这里都是用的plt,编译器会自动为plt添加got表项,然后对got表中重定位对应的函数。而plt的作用是延迟加载,系统的start.S(编译成Scrt1.o )认为这里调用main, __libc_start_main没有必要弄成延迟调用。不延迟调用,那我们可以直接将重定位到got表中的main地址取出放入rdi,再直接间接调用got表中__libc_start_main地址,写法参考https://zhuanlan.zhihu.com/p/404925251](https://zhuanlan.zhihu.com/p/404925251)。初始版的start.S中两条指令可改为如下。

movq fun@GOTPCREL(%rip),%rdi
callq *__libc_start_main@GOTPCREL(%rip)

链接时用到的几个object文件

在Makefile中,我们之间指定不让用-nostartfiles, 那构建时都有哪些start的object文件呢?如下,

这几个文件全部在/usr/lib/x86_64-linux-gnu/

Scrt1.o

定义了_start函数,里面通过__libc_start_main函数调用main, atexit。

crti.o

定义了_init, _fini 函数,分别在.init段和.fini段中。init函数中gprof会用。默认其实用不到

crtbeginS.o

定义了 deregister_tm_clonesregister_tm_clones, __do_global_dtors_aux,函数

crtendS.o

看到是空的。

crtn.o

两个段,.init, .fini

链接成exe时,crti.ocrtn.o.init, .fini会分别进行拼接成最终的段,若链接的object文件中也有这两个段,结果应该也是将它们加到一起。