关于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_clones
,register_tm_clones
, __do_global_dtors_aux
,函数
crtendS.o
看到是空的。
crtn.o
两个段,.init
, .fini
链接成exe时,crti.o
和crtn.o
的.init
, .fini
会分别进行拼接成最终的段,若链接的object文件中也有这两个段,结果应该也是将它们加到一起。