ld有多种方法设置进程入口地址, 按一下顺序: (编号越前, 优先级越高)
1, ld命令行的-e选项
2, 连接脚本的ENTRY(SYMBOL)命令
eg. ENTRY(_start) /* Entry point of application*/
3, 如果定义了start符号, 使用_start符号值
4, 如果存在.text section, 使用.text section的第一字节的位置值
5, 使用值0
(一)通常例子
#include
int main()
{
printf(“helloworld! \n”);
return 0;
}
$ gcc hello.c -o hello
$ ./hello
用户的代码是从main函数开始执行的,还有其它很多函数,比如_start函数。实际上程序真正的入口并不是main函数,我们以下面命令对hello.c代码进行编译:
$ gcc hello.c -nostdlib
/usr/bin/ld: warning: cannot find entrysymbol _start; defaulting to 0000000000400144
-nostdlib命令是指不链接标准库,报错说找不到entry symbol _start,这里是说找不到入口符号_start,也就是说程序的真正入口是_start函数。
实际上main函数只是用户代码的入口,它会由系统库去调用,在main函数之前,系统库会做一些初始化工作,比如分配全局变量的内存,初始化堆、线程等,当main函数执行完后,会通过exit()函数做一些清理工作,用户可以自己实现_start函数:
(二)通过 _start 来实现
#include
#include
int _start(void)
{
printf(“hello world!\n”);
exit(0);
}
执行如下编译命令并运行:
$ gcc hello_start.c -nostartfiles -o hello_start
$ ./hello_start
hello world!
$ readelf -al a.out
…
Symbol table ‘.dynsym’ contains 3 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2)
…
//查看elf文件 参数只有一个,printf 被优化成了puts
$ readelf -al a.out | grep FUN
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2)
22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
25: 0000000000400390 24 FUNC GLOBAL DEFAULT 10 _start
27: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@@GLIBC_2.2.5
(三)通过链接的时候指定来实现
#include
#include
int mymain()
{
printf(“helloworld!\n”);
exit(0);
}
$ gcc hello_nomain.c -nostartfiles -e mymain -o hello_mymain
其中-e选项可以指定程序入口符号,查看符号表如下:
$ readelf -s hello_mymain | grep FUNC |
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@GLIBC_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@GLIBC_2.2.5 (2)
22: 0000000000000000 0 FUNC GLOBAL DEFAULT UND puts@@GLIBC_2.2.5
24: 0000000000400390 24 FUNC GLOBAL DEFAULT 10 mymain
27: 0000000000000000 0 FUNC GLOBAL DEFAULT UND exit@@GLIBC_2.2.5
//elf header 中的 入口 Entry point address 就是 函数的开始地址
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2’s complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x400390
Start of program headers: 64 (bytes into file)
Start of section headers: 5184 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 20
Section header string table index: 17
三 关于 uboot start.s
通过连接脚本,把 start.o 放在代码段的 最开始的地方,让他好最先执行
SECTIONS
{
…
.text:
{
start.o(.text)
misc.o(.text)
.text
*.text.
}
程序并不是从main函数开始执行的,gcc -o main main.c时,默认会连接libc.so(可以指定-nodefaultlib, -nostdlib取消连接),并且会添加一个启动代码_start函数(可以指定-nodefaultlib, -nostdlib不添加启动代码),用于初始化,并提供main函数的argc, argv等参数,_start函数中会调用main函数。
注意
1、-Ttext-segment指定的必需是一个页对齐的地址。
2、动态库的装载位置不是固定的,一般可以认为动态库的-Ttext-segment=0,没有使用;如果想直接运行.so的话(glibc里面很多.so都可以直接运行),需要为.so指定ld-linux.so(.so默认没有指定):
gcc -shared -fPIC -o libx.so x.c -Wl,–dynamic-linker=some_ld_linux.so.x
或者代码里面写:const char my_interp[] attribute((section(“.interp”))) = “/lib/ld-linux.so.3”; //需要保证这个ld-linux.so存在且能用
可以使用readelf -l查看.inter段,也就是指定的ld-linux.so
3、-Ttext-segment并不是指定.text段的加载位置,而是指定整个elf的加载位置。
如果想指定.text段的加载位置,可以:
-Tbss=org
-Tdata=org
-Ttext=org
Same as –section-start, with “.bss”, “.data” or “.text” as the
sectionname.
4、gcc -Wl,-Ttext-segment=0x400000 -Wl,-Ttext=0x800000 -o x x.c这样是可以的,但是如果-Ttext指定的比较小,要么程序无法运行,要么可能和其他段冲突。加完-Ttext之后,.text段以及后面的段,在文件中的偏移会变大不少,中间填充的0是为了加载到内存后的页对齐,在内存中的加载位置都会从-Ttext指定的位置开始。
linux系统下压板程序的入口是”_start”,这个函数是linux系统库(Glibc)的一部分,当我们的程序和Glibc链接在一起形成最终的可执行文件的之后,这个函数就是程序执行初始化的入口函数。
程序初始化部分完成一系列初始化过程之后,会调用main函数来执行程序的主体。在main函数执行完成以后,再返回到初始化部分,进行一些清理工作,然后结束进程。
对C++而言:(ELF文件为其定义了两个特殊的段)
.init 该段保存的是可执行的命令,它构成了进程的初始化代码。因此,当一个程序开始运行的时候,在main函数被调用之前,Glibc的初始化部分安排执行这个段中的代码
.fini 该段保存着进程终止命令代码。因此,当一个程序的main函数正常退出的时候,Glibc会安排执行这个段中的代码。
这两个段的存在有特别的目的,如果一个函数放到.init段,在mai函数执行前系统就会执行它(就是因为它在这个段)。同理,如果一个函数放到.fini段,在main函数返回后该函数就会被执行。利用这两个特性,C++实现了全局构造和析构函数。
一个典型程序的大致运行步骤
操作系统创建进程后,把控制权交到了程序入口,这个入口往往是程序运行库中的某个入口函数。
入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量的构造等等。
入口函数在完成初始化之后,调用main函数,正式开始执行函数主体部分。
main函数执行完毕之后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。
入口函数的实现
Glibc的入口函数
_start函数
该入口是由ld链接器默认的链接脚本指定的,当然用户也可以通过参数进行设定。_start由汇编代码实现。大致用如下伪代码表示:
void _start()
{
%ebp = 0;
int argc = pop from stack
char ** argv = top of stack;
__libc_start_main(main, argc, argv, __libc_csu_init, __linc_csu_fini,
edx, top of stack);
}
具体过程可以参见下图:
在调用_start之前,装载器就会将用户的参数和环境变量压入栈中,如图所示,栈顶元素是argc,接着就是argv和环境变量的数组。
其中argv除了指向参数表外,还隐含紧接着环境变量表。这个环境变量表要在__libc_start_main里从argv内提取出来。
实际执行代码的是__libc_start_main。
char **ubp_rv = &ubp_av[argc+1];
__environ = ubo_ev;
__libc_stack_end = stack_end;
result = main(argc, argv, _environ);
exit(result);
在执行main之前全局变量已经初始化,main函数的两个参数也被正确传了进来,堆和栈的初始化也已经完成,一些系统I/O也被初始化。
完成上面这些工作的函数称为入口函数(Entry Point)。一个典型的运行步骤大致如下:
·操作系统在创建进程后,把控制权交到了程序的入口,这个入口往往是运行库中的某个函数。
·入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造等。
·入口函数在完成初始化之后,调用main函数,正是开始执行程序主体部分
·main函数执行完毕以后,返回到入口函数,入口函数进行清理工作,包括全局变量的析构,堆销毁、关闭I/O等。然后进行系统调用结束进程
Glibc的入口函数:
_start
在调用_start前,装载器把用户参数和环境变量压入栈中,按照其压栈的方法,实际上栈顶的元素是argc,而接着其下就是argc和环境变量数组。
_start大概的功能可以用下面的代码描述:
void _start()
{
%ebp = 0;
int argc = pop from stack
char ** argv = top of stack;
__libc_start_main(main, argc, argv, __libc_csu_init, __linc_csu_fini,
edx, top of stack);
}
其中argv除了指向参数表之外,还隐含紧接着环境变量表。这个环境变量表在__libc_start_main里从argv内部提取出来。
MSVC的入口函数:
int mainCRTStartup(void)
{
…
}
在该函数中使用了alloca进行内存分配,这是因为堆还没有初始化,而alloca是唯一可以不使用堆的动态分配机制的函数。
alloca可以再栈上分配任曦大小的空间(只要栈允许),并且在函数放回的时候自动释放,好像局部变量一样。
mainCRTStartup 的总体流程就是:
1.初始化和OS版本有关的全局变量
2.初始化堆
3.初始化I/O
4.获取命令行参数和环境变量
5.初始化C库的一些数据
6.调用main并记录返回值
7.检查错误并将main的返回值返回
MSVC CRT 的入口函数初始化
MSVC的入口函数初始化主要包含两部分,堆初始化和I/O初始化。MSVC的对初始化由函数_heap_init完成(调用HeapCreate)。
I/O初始化工作比较复杂,主要进行如下几个工作:
·建立打开的文件表
·如果能够继承自父进程,那么从父进程获取继承的句柄
·初始化标准输入输出
C语言运行库(C Runtime Library):
C运行库大致包含如下功能:
·启动与退出:包括入口函数及入口函数所依赖的其他函数等
·标准函数:由C语言标准规定的C语言标准库所拥有的函数实现
·I/O:I/O功能的封装和实现
·堆:堆的封装和实现
·语言实现:语言中的一些特殊功能的实现
·调试:实现调试功能的代码
C语言的标准库:(ANSI C的标准库由24个C头文件组成)
·标准输入和输出(stdio.h)
·文件操作(stdio.h)
·字符操作(ctype.h)
·字符串操作(string.h)
·数学函数(math.h)
·资源管理(stdlib.h)
·格式转换(stdlib.h)
·时间/日期(time.h)
·断言(assert.h)
·各种类型上的常数(limits.h & float.h)
·变长参数(stdarg.h)
·非局部跳转(setjmp.h)