文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

C语言不支持重载,多种main()如何实现的呢?

2024-12-01 15:48

关注

大家都知道,我是做上层应用的,对底层不是很了解,更别说那帮人在讨论内核的时候,根本插不上话。更多的时候,还是默默记笔记,紧跟大佬们的步伐😁。

于是,为了调研这个问题,也查了相关资料。今天借助本文,来分析下C语言中main()的实现,顺便解答下群里的这个问题。

定义

作为C/C++开发人员,都知道main()函数是一个可执行程序的入口函数,大都会像如下这样写:

int main() {}
int main(int argc, char *argv[]) {}

但是,作为一个开发老油条,也仅仅知道是这样做的,当看到二哥提出这个问题的时候,第一反应是重载,但是大家都知道C语言是不支持重载的,那么有没有可能使用的是默认参数呢?如下这种:

int main(int argc = 1, char **argv = NULL)

好了,为了验证我的疑问,咱们着手开始进行分析。

ps:在cppreference上对于main()的声明有第三个参数即char *envp[],该参数是环境变量相关,因为我们使用更多的是不涉及此参数的方式,所以该参数不在本文的讨论范围内。

断点调试

为了能够更清晰的理解main()函数的执行过程,写了一个简单的代码,通过gdb查看堆栈信息,代码如下:

int main() {
return 0;
}

编译之后,我们通过gdb进行调试,在main()函数处设置断点,然后看堆栈信息,如下:

(gdb) bt
#0 main () at main.c:2
(gdb)

从上述gdb信息,我们看出main()位于栈顶,显然,我们的目的是分析main()的调用堆栈信息,而这种main()在栈顶的方式显然不足以解答我的疑问。

于是,查阅了相关资料后,发现可以通过其它方式打印出更详细的堆栈信息。

编译命令如下:

gcc -gdwarf-5 main.c  -o main

然后gdb的相关命令(具体的命令可以网上查阅,此处不做过多分析):

gdb ./main -q
Reading symbols from /mtad/main...done.
(gdb) set backtrace past-entry
(gdb) set backtrace past-main
(gdb) show backtrace past-entry
Whether backtraces should continue past the entry point of a program is on.
(gdb) show backtrace past-main
Whether backtraces should continue past "main" is on.

然后在main()处设置断点,运行,查看堆栈信息,如下:

(gdb) bt
#0 main () at main.c:2
#1 0x00007ffff7a2f555 in __libc_start_main () from /lib64/libc.so.6
#2 0x0000000000400429 in _start ()
(gdb)

通过如上堆栈信息,我们看到_start()-->__libc_start_main()-->main(),看来应该在这俩函数中,开始分析~~

_start()

为了查看_start()的详细信息,继续在_start()函数处打上断点,然后分析查看:

(gdb) r
Starting program: xxx
Missing separate debuginfos, use: debuginfo-install glibc-2.17-317.el7.x86_64
Breakpoint 1, 0x0000000000400400 in _start ()
(gdb) s
Single stepping until exit from function _start,
which has no line number information.
0x00007ffff7a2f460 in __libc_start_main () from /lib64/libc.so.6

通过如上分析,没有看到_start()函数的可执行代码,于是通过网上搜索,发现_start()是用汇编编写,于是下载了glibc2.5源码,在路径处sysdeps/i386/elf/start.S

#include "bp-sym.h"
.text
.globl _start
.type _start,@function
_start:

xorl %ebp, %ebp

popl %esi
movl %esp, %ecx

andl $0xfffffff0, %esp
pushl %eax

pushl %esp
pushl %edx
#ifdef SHARED

call 1f
addl $_GLOBAL_OFFSET_TABLE_, %ebx

leal __libc_csu_fini@GOTOFF(%ebx), %eax
pushl %eax
leal __libc_csu_init@GOTOFF(%ebx), %eax
pushl %eax
pushl %ecx
pushl %esi
pushl BP_SYM (main)@GOT(%ebx)

call BP_SYM (__libc_start_main)@PLT
#else

pushl $__libc_csu_fini
pushl $__libc_csu_init
pushl %ecx
pushl %esi
pushl $BP_SYM (main)

call BP_SYM (__libc_start_main)
#endif
hlt
#ifdef SHARED
1: movl (%esp), %ebx
ret
#endif

.section .rodata
.globl _fp_hw
_fp_hw: .long 3
.size _fp_hw, 4
.type _fp_hw,@object

.data
.globl __data_start
__data_start:
.long 0
.weak data_start
data_start = __data_start

上述实现也是比较简单的:

xorl %ebp, %ebp:将ebp寄存器清零。

popl %esi、movl %esp, %ecx:装载器把用户的参数和环境变量压栈,实际上按照压栈的方法,栈顶的元素就是argc,接着其下就是argv和环境变量的数组。这两句相当于int argc = pop from stack; char **argv = top of stack。

call BP_SYM (__libc_start_main):相当于调用__libc_start_main,调用的时候传入参数,包括argc、argv。

上述逻辑功能,伪代码实现如下:

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

__libc_start_main

在上一节中,我们了解到,_start()才是整个可执行程序的入口函数,在_start()函数中调用__libc_start_main()函数,该函数声明如下:

STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
int argc, char *__unbounded *__unbounded ubp_av,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
ElfW(auxv_t) *__unbounded auxvec,
#endif
__typeof (main) init,
void (*fini) (void),
void (*rtld_fini) (void), void *__unbounded stack_end)
{
#if __BOUNDED_POINTERS__
char **argv;
#else
# define argv ubp_av
#endif

int result;
__libc_multiple_libcs = &_dl_starting_up && !_dl_starting_up;
...
...
if (init)
(*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);
...
result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
exit (result);
}

可以看出,在该函数中,最终调用了main()函数,并传入了相关命令行。(result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);)

截止到此,我们了解了整个main()函数的调用过程,但是,仍然没有回答二哥的问题,main()是如何实现有参和无参两种方式的,其实说白了,在标准中,main()只有一种声明方式,即有参方式。无论是否有命令行参数,都调用该函数。如果有参数,则通过压栈出栈(对于x86 32位)或者寄存器(x86 64位)的方式获取参数,然后传入main(),如果命令行为空,则对应的字段为空(即没有从栈上取得对应的数据)。

来源:C语言与C++编程内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯