main函数和启动例程

汇编程序的入口是_start,而C程序的入口是main函数。

汇编和链接步骤是:

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 main.o

-S选项生成汇编代码, -c选项生成目标文件,此外-E选项只做预处理而不编译,如果不加这些选项则gcc执行完整的编译步骤,直到最后链接生成可执行文件为止。如下图所示:

这些选项都可以和-o搭配使用,给输出的文件重新命名而不使用gcc默认的文件名。

如果目标文件是由C代码编译生成的,用gcc做链接就没错了,整个程序的入口点是crt1.o中提供的_start,它首先做一些初始化工作(以下称为启动例程, Startup Routine),然后调用C代码中提供的main函数。所以,以前我们说main函数是程序的入口点其实不准确,_start才是真正的入口点,而main函数是被_start调用的

以下面的代码为例:

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;
}

如果分两步编译:

gcc -c main.c -o main.o
gcc main.o -o main

其实上面的第二步是调用ld做链接的,相当于这样的命令:

ld /usr/lib/crt1.o /usr/lib/crti.o main.o -o main -lc -dynamiclinker /lib/ld-linux.so.2

也就是说,除了crt1.o之外其实还有crti.o,这两个目标文件和我们的main.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中这条指令暂时写成push符号main所代表的地址,但不知道这个地址是多少,所以在crt1.o中这条指令暂时写成push0x0,等到和main.o链接成可执行文件时就知道这个地址是多少了,比如是0x80483c4,那么可执行文件main中的这条指令就被链接器改成了push $0x80483c4。链接器在这里起到符号解析(Symbol Resolution)的作用,除此之外有时候链接器还可以起到重定位的作用,这两种作用都是通过修改指令中的地址实现的,链接器也是一种编辑器, vi和emacs编辑的是源文件,而链接器编辑的是目标文件,所以链接器也叫Link Editor。T _start这一行表示_start这个符号在crt1.o中提供了定义,这个符号的类型是代码(T表示Text)。我们从上面的输出结果中选取几个符号用图示说明它们之间的关系:

其实上面我们写的ld命令做了很多简化, gcc在链接时还用到了另外几个目标文件,所以上图多画了一个框,表示组成可执行文件main的除了main.o、 crt1.o和crti.o之外还有其它目标文件。

gcc的-v选项可以了解详细的编译过程。

复制代码

gcc -v main.c -o main
Using built-in specs.
Target: i486-linux-gnu
...
/usr/lib/gcc/i486-linux-gnu/4.3.2/cc1 -quiet -v main.c -
D_FORTIFY_SOURCE=2 -quiet -dumpbase main.c -mtune=generic -auxbase
main -version -fstack-protector -o /tmp/ccRGDpua.s
...
as -V -Qy -o /tmp/ccidnZ1d.o /tmp/ccRGDpua.s
...
/usr/lib/gcc/i486-linux-gnu/4.3.2/collect2 --eh-frame-hdr -m
elf_i386 --hash-style=both -dynamic-linker /lib/ld-linux.so.2 -o
main -z relro /usr/lib/gcc/i486-linuxgnu/4.3.2/../../../../lib/crt1.o /usr/lib/gcc/i486-linuxgnu/4.3.2/../../../../lib/crti.o /usr/lib/gcc/i486-linuxgnu/4.3.2/crtbegin.o -L/usr/lib/gcc/i486-inux-gnu/4.3.2 -
L/usr/lib/gcc/i486-linux-gnu/4.3.2 -L/usr/lib/gcc/i486-linuxgnu/4.3.2/../../../../lib -L/lib/../lib -L/usr/lib/../lib -
L/usr/lib/gcc/i486-linux-gnu/4.3.2/../../.. /tmp/ccidnZ1d.o -lgcc --
as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --
no-as-needed /usr/lib/gcc/i486-linux-gnu/4.3.2/crtend.o
/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crtn.o

复制代码

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

复制代码

objdump -d main
main: file format elf32-i386
Disassembly of section .init:
08048274 <_init>:
8048274: 55 push %ebp
8048275: 89 e5 mov %esp,%ebp
8048277: 53 push %ebx
...
Disassembly of section .text:
080482e0 <_start>:
80482e0: 31 ed xor %ebp,%ebp
80482e2: 5e pop %esi
80482e3: 89 e1 mov %esp,%ecx
...
08048394 <bar>:
8048394: 55 push %ebp
8048395: 89 e5 mov %esp,%ebp
8048397: 83 ec 10 sub $0x10,%esp
...
080483aa <foo>:
80483aa: 55 push %ebp
80483ab: 89 e5 mov %esp,%ebp
80483ad: 83 ec 08 sub $0x8,%esp
...
080483c4 <main>:
80483c4: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483c8: 83 e4 f0 and $0xfffffff0,%esp
80483cb: ff 71 fc pushl -0x4(%ecx)
...
Disassembly of section .fini:
0804849c <_fini>:
804849c: 55 push %ebp
804849d: 89 e5 mov %esp,%ebp
804849f: 53 push %ebx

复制代码

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的反汇编:

复制代码

...
Disassembly of section .text:
080482e0 <_start>:
80482e0: 31 ed xor %ebp,%ebp
80482e2: 5e pop %esi
80482e3: 89 e1 mov %esp,%ecx
80482e5: 83 e4 f0 and $0xfffffff0,%esp
80482e8: 50 push %eax
80482e9: 54 push %esp
80482ea: 52 push %edx
80482eb: 68 00 84 04 08 push $0x8048400
80482f0: 68 10 84 04 08 push $0x8048410
80482f5: 51 push %ecx
80482f6: 56 push %esi
80482f7: 68 c4 83 04 08 push $0x80483c4
80482fc: e8 c3 ff ff ff call 80482c4
<__libc_start_main@plt>
...

复制代码

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

复制代码

Disassembly of section .plt:
...
080482c4 <__libc_start_main@plt>:
80482c4: ff 25 04 a0 04 08 jmp *0x804a004
80482ca: 68 08 00 00 00 push $0x8
80482cf: e9 d0 ff ff ff jmp 80482a4 <_init+0x30>

 

复制代码

这三条指令位于.plt段而不是.text段, .plt段协助完成动态链接的过程。

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

exit(main(argc, argv));

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

#include <stdlib.h>
int main(void)
{
    exit(4);
}

这样和int main(void) { return 4; }的效果是一样的。在Shell中运行这个程序并查看它的退出状态:

./a.out
echo $?
4

按照惯例,退出状态为0表示程序执行成功,退出状态非0表示出错。注意,退出状态只有8位,而且被Shell解释成无符号数,如果将上面的代码改为exit(-1);或return -1;,则运行结果为:

./a.out
echo $?
255

注意:如果声明一个函数的返回值类型是int,函数中每个分支控制流程必须写return语句指定返回值,如果缺了return则返回值不确定(想想这是为什么),编译器通常是会报警告的,但如果个分支控制流程调用了exit或_exit而不写return,编译器是允许的,因为它都没有机会返回了,指不指定返回值也就无所谓了。使用exit函数需要包含头文件stdlib.h,而使用_exit函数需要包含头文件unistd.h。

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
sprintf和snprintf都是C标准库中的字符串处理函数,用于将格式化的字符串输出到字符数组中。 sprintf函数的原型为: ```c int sprintf(char* str, const char* format, ...); ``` 其中,第一个参数是一个字符数组,用于存放输出的字符串;第二个参数是一个格式化字符串,用于指定输出的格式;后面的参数是可选的,用于指定格式化字符串中的占位符。 以下是一个使用sprintf函数的例子: ```c #include <stdio.h> int main() { char str[50]; int num = 123; sprintf(str, "The number is %d", num); printf("%s\n", str); return 0; } ``` 输出结果为: ``` The number is 123 ``` 在这个例子中,我们声明了一个长度为50的字符数组str,用于存放输出的字符串。然后使用sprintf函数将格式化的字符串输出到str中,并将结果打印出来。 snprintf函数的原型为: ```c int snprintf(char* str, size_t size, const char* format, ...); ``` 其中,第一个参数和第二个参数与sprintf函数相同,第三个参数也是格式化字符串,后面的参数也是可选的。与sprintf函数不同的是,在snprintf函数中,我们需要指定输出字符串的长度,以避免输出字符串过长导致数组越界。 以下是一个使用snprintf函数的例子: ```c #include <stdio.h> int main() { char str[10]; int num = 123; snprintf(str, 10, "%d", num); printf("%s\n", str); return 0; } ``` 输出结果为: ``` 123 ``` 在这个例子中,我们声明了一个长度为10的字符数组str,使用snprintf函数将num转换为字符串,并将结果输出到str中。由于我们指定了输出字符串的长度为10,因此snprintf函数最多只会输出10个字符,避免了数组越界的问题。 需要注意的是,使用snprintf函数时,我们需要确保输出字符串的长度足够大,以容纳输出的内容,否则可能会导致输出内容被截断。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值