main

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。



  • __libc_start_main函数

    • 函数头
      ```
      int __libc_start_main(
      int (main)(int, char **, char *),
      char * __unbounded *__unbounded ubp_av,
      __typeof(main) init,
      void (
      fini)(void),
      void (*rtld_fini)(void),
      viud *__unbounded stack_end)
        可以啊看出,一共有7个参数,其中main由第一个参数传入,紧接着就是argc和argv(这里叫做ubp_av,应为其中还包括了环境变量表)。此外的3个函数指针:
      (1)init:main调用之前的初始化工作;
      (2)fini:main结束之后的收尾工作;
      (3)rtld_fini:和动态加载有关的收尾工作。
      最后的stack_end标明了栈底的位置,即最高的栈地址。

      • __libc_start_main代码中的一个特殊的宏(宏INIT_ARGV_and_ENVIRON)
        宏展开之后如下:
        char **ubp_rv = &ubp_av[argc+1];
        __environ = ubo_ev;
        __libc_stack_end = stack_end;
          上述代码实际上就是从_start源代码分析得到的栈布局,重点是让_environ指针指向紧跟子啊argv数组后面的环境变量数组。如下图:

      • __libc_start_main代码中的一系列重要的函数
        ```
        __pthread_initialize_minimal();
        __cxa_atexit(rtld_fini, NULL, NULL);
        __libc_init_first(argc, argv, __environ);
        __cxa_atexit(fini, NULL, NULL);
        (*init)(argc, argv, __environ);

      • __cxa_atexit函数是glibc的内部函数,等同于atexit,在main之后调用。

      • 所以可以看出,参数传入的fini和rtld_fini均是用于main结束之后调用的。在__libc_start_main末尾,关键是如下两行的代码:
        result = main(argc, argv, _environ);
        exit(result);
        main函数最终被调用,并退出。
        【补充】程序正常结束有两种情况:main函数正常返回;程序中exit()退出。但是在__libc_start_main中可以看出,即使main正常返回了,exit还是会被调用。所以说exit()是程序退出的必经之路。







在执行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)


Category linux