[汇编与C语言关系]2. main函数与启动例程

  为什么汇编程序的入口是_start,而C程序的入口是main函数呢?以下就来解释这个问题

  在《x86汇编程序基础(AT&T语法)》一文中我们汇编和链接的步骤是:

$ as hello.s -o hello.o
$ ld hello.o -o hello

  我们用gcc main.c -o main开编译一个c程序,其实际分为三个步骤:编译、汇编、链接

$ gcc -S main.c   生成汇编代码
$ gcc -c main.s   生成目标文件
$ gcc main.o    生成可执行文件

  我们先前在《x86汇编程序基础(AT&T语法)》中由第一个汇编程序生成的目标文件hello.o我们使用ld来链接的,那能不能用gcc呢?如下:

  

  报了两个错误:1. _start有多处定义,一个定义是我们汇编代码中的。另一个定义来自/usr/lib/cr1l.o;2. crt1.o的_start函数要调用main函数,而我们的汇编代码中没有提供main函数的定义。最后一行显示这些错误提示是ld报出的。所以如果我们用gcc做链接,gcc实际是调用ld将目标文件crt1.o和我们写的hello.o链接在一起。

  如果目标文件是由C程序编译生成的,用gcc做链接就没错了,整个程序的入口是crtl.o中提供的_start,它首先做一些初始化操作(以下称为启动例程,Startup Routine),然后调用C代码中提供的main函数。_start才是真正的入口点,main是被_start调用的。  我们继续上一篇文章《[汇编与C语言关系]1.函数调用》研究,gcc main.o -o main其实是调用ld做链接的,相当于这样的命令:

$ ld /usr/lib/crt1.o /usr/lib/crti.o main.o -o main -lc -dynamic-linker /lib/ld-linux.so.2

  除了crt1.o之外还有crti.o,这两个目标文件和我们的hello.o链接在一起生成可执行文件main。-lc表示需要链接libc库,-lc选项是gcc默认的,不用写,而对于ld则不是默认选项,所以要写上。-dynamic-linker /lib/ld-linux.so.2指定动态链接器是/lib/ld-linux.so.2。

  我们可以用readelf查看crt1.o和crti.o里面的内容。在这里我们只关心符号表,如果只看符号表,可以用readelf命令的-s选项,也可以用nm命令。

  

$ nm /usr/lib/crt1.o
00000000 R _IO_stdin_used
00000000 D __data_start
             U __libc_csu_fini
             U __libc_csu_init
             U __libc_start_main
00000000 R _fp_hw
00000000 T _start
00000000 W data_start
U main
$ nm /usr/lib/crti.o
              U _GLOBAL_OFFSET_TABLE_
              w __gmon_start__
00000000 T _fini
00000000 T _init

  U main这一行表示main这个符号在crt1.o中用到了,但是没有定义(U表示Undefined),因此需要别的目标文件提供一个定义并且和crt1.o链接在一起。具体来说,在crt1.o中要用到main这个符号所代表的地址,例如有一条指令是push $符号main所代表的地址, 但不知道这个地址是多少,所以在crt1.o中这条指令暂时写成$0x0,等到和main.o链接成可执行文件时就知道这个地址是多少了,比如是0x80483c4,那么可执行文件main中的这条指令就被链接器改成push $0x80483c4。链接器在这里起到符号解析(Symbol Resolution)的作用。链接器还有一种作用就是重定位作用,而链接器编辑的是目标文件,所以链接器也是一种编辑器,vi等其他编辑器编辑的是源文件,而链接器编辑的是目标文件,所以链接器也叫Link Editor。T _start这一行表示_start这个符号在crt1.o中提供了定义,这个符号的类型是代码(T表示Text)。我们从上面的输出结果中选取几个符号用图示说明它们之间的关系:

  

  上边我们写的ld命令做了简化,gcc在链接过程中还用到了其他几个目标文件,所以上图多画了一个框,表示组成可执行文件main的除了main.o、crt1.o和crti.o之外还有其他目标文件,gcc -v选项可以了解详细的编译过程。

  链接生成的可执行文件main中包含了各目标文件所定义的符号, 通过反汇编可以看到这些符号的定义:

  crt1.o中的未定义符号main在main.o中定义了,所以链接在一起就没有问题了。crt1.o还有一个未定义符号_libc_start_main在其他几个目标文件中也没有定义,所以在可执行文件main中仍然是个未定义符号。这个符号是在libc中定义的,libc并不像其他目标文件一样链接到可执行文件main中,而是在运行时做动态链接:

    1.操作系统在加载执行main这个程序时,首先查看它有没有需要动态链接的未定义符号。

    2. 如果需要做动态链接,就查看这个程序制定了哪些共享库(我们用-lc指定了libc)以及用什么动态链接器来做动态链接(我们用 -dynamic-linker /lib/ld-linux.so.2指定了动态链接器)。

    3. 动态链接器在共享库中查找这些符号的定义,完成链接过程。

  了解了这些以后我们来看_start的反汇编:

  

  首先将一系列参数压栈,然后调用libc的库函数__libc_start_main做初始化工作,其中最后一个压栈的参数push $0x80483c4是main函数的地址,__libc_start_main在完成初始化工作之后会调用main函数。由于__libc_start_main需要动态链接,所以这个库函数的指令可以在可执行文件main的反汇编中肯定是找不到的,然而我们找到了这个:

  

  一开始看到这以为是libc被链接进去了,其实不是。这三条指令位于.plt段不是.text段,.plt段协助完成动态链接的过程。

  main函数的原型是int main(int argc, char *argv[]),也就是说启动例程会传两个参数给main函数。

  由于main函数是被启动例程调用的,所以从main函数return时仍返回到启动例程中,main函数的返回值被启动例程得到,如果将启动例程表示成等价的C代码(实际上启动例程一般是直接用汇编写的),则它调用main函数的形式是:

exit(main(argc, argv));

  也就是说启动例程得到main函数的返回值后,会立刻用它做参数调用exit函数。exit也是lib中的函数,它首先做一些清理工作,然后调用_exit系统调用终止进程,main函数的返回值最终被传给_exit系统调用,成为进程的退出状态。我们也可以在main函数中直接调用exit函数终止进程而不返回到启动例程。

  注意,退出状态只有8位,而且被Shell解释成无符号数,如果将上面的代码改为exit(-1);或return -1;则echo $?会输出255。

  使用_exit函数需要包含头文件unistd.h。

时间: 2024-10-11 16:29:11

[汇编与C语言关系]2. main函数与启动例程的相关文章

第七章之main函数和启动例程

main函数和启动例程 为什么汇编程序的入口是_start,而C程序的入口是main函数呢?本节就来解释这个问题.在讲例 18.1 "最简单的汇编程序"时,我们的汇编和链接步骤是: $ as hello.s -o hello.o$ ld hello.o -o hello以前我们常用gcc main.c -o main命令编译一个程序,其实也可以分三步做,第一步生成汇编代码,第二步生成目标文件,第三步生成可执行文件: $ gcc -S main.c$ gcc -c main.s$ gcc

go语言基础(main函数、数据类型)

go语言基础(main函数.数据类型) 1.Go语言介绍 Go语言是云计算时代的c语言 c和c++这类语言提供了很快的执行速度,而Rudy和python这类语言则擅长快速开发.Go语言则介于两者之间,不仅提供了高性能的语言,同时也让开发更快速 优势 部署简单,可直接编译成机器码.不依赖其他库,部署就是扔一个文件上去就完成了 静态类型语言(c是静态语言.python解释性语言),但是有动态语言的感觉,静态类型的语言就是可以在编译的时候检查出来隐藏的大多数问题,动态语言的感觉就是有很多的包可以使用,

C语言编程漫谈——main函数

写在前面 促使我写这篇文章是因为我这几天找了几个一样是大三的同学,与我相同专业相同方向(物联网)的人,除了@小胡同的诗,基本没有什么其他人会现在看起来很简单的编程题目了.问了一下其他同学,他们大部分都说自己C语言是混过的,因为之前老师教的时候说不会指针||结构体||函数 不会写也没有关系,然后他们居然还能过省二级(C语言)???结果到了大三,没人给他们做大腿了,所以基本上课堂上就进入了“休眠模式”,后面的东西基本都听不懂了……所以来这里给大一大二还在学习C语言的同学一点小小的建议.这样才能编的下

[汇编与C语言关系]3. 变量的存储布局

以下面C程序为例: #include <stdio.h> const int A = 10; int a = 20; static int b = 30; int c; int main(void) { static int a = 40; char b[] = "Hello World"; register int c = 50; printf("Hello World%d\n", c); return 0; } 我们在全局作用域和main函数的局部作

[汇编与C语言关系]1.函数调用

对于以下程序: int bar(int c, int d) { int e = c + d; return e; } int foo(int a, int b) { return bar(a, b); } int main(void) { foo(2, 3); return 0; } 在编译时加上-g选项,用objdump反汇编时可以把C代码和汇编代码穿插起来显示: 反汇编的结果很长以下是截取要分析的部分: 整个程序的执行过程是main调用foo, foo调用bar, 用gdb跟踪程序的执行,直

[汇编与C语言关系]4. 结构体和联合体

用反汇编的方法研究一下C语言的结构体: #include <stdio.h> int main(int argc, char ** argv) { struct { char a; short b; int c; char d; } s; s.a = 1; s.b = 2; s.c = 3; s.d = 4; printf("%u\n", sizeof(s)); return 0; } main函数中几条语句的反汇编结果如下: 从访问结构体成员的指令可以看出,结构体的四个成

[汇编与C语言关系]5. volatile限定符

现在研究一下编译器优化会对生成的指令产生什么影响,在此基础上介绍C语言的volatile限定符.首先看下面的C程序: /* artificial device registers */ unsigned char recv; unsigned char send; /* memory buffer */ unsigned char buf[3]; int main(void) { buf[0] = recv; buf[1] = recv; buf[2] = recv; send = ~buf[0

c语言:使用main函数的参数,实现一个整数计算器

/* 使用main函数的参数,实现一个整数计算器,程序可以接受三个参数, 第一个参数"-a"选项执行加法,"-s"选项执行减法, "-m"选项执行乘法,"-d"选项执行除法,后面两个参数为操作数. */ #include<stdio.h> #include<stdlib.h> int my_calculator(char *p,int num1,int num2)//calculator表示计算器 {

c语言数组放在main函数里面和外面的区别

最近a算法题的时候碰到一道题:一个数列前三项都为1,之后每项的值等于前三项之和,求第20193024项的最后4位数字.一开始写的代码如下: 结果一直爆 Terminated due to signal: SEGMENTATION FAULT (11) 这个错误,改了好久没改出来.之后在stackoverflow上提问才找到自己错误所在. 这里先介绍一下栈区(stack),堆区(heap),数据区(data seg)和代码区. 栈区:由操作系统自动分配释放,存放函数的参数值,局部变量的值:当不需要