main()是程序运行的第一个函数吗?

本文深入探讨了程序在运行前CPU所做的准备工作,包括编译、链接、重定位和虚拟地址的概念,以及main()函数在C语言中的作用和特性。重点讲述了如何通过虚拟内存管理提高系统安全性,以及执行环境在程序执行过程中的作用。
    程序运行到main()函数之前,CPU做了那些事情?main()是程序运行的第一个函数吗?

    你是否真的明白——编译、链接、重定位以及虚拟地址?

    或许,你不需要知道和理解这些知识就能够编程,而且工资还很高,但这些都不是你不知道的理由。
   
    在早期的计算机中,由于所有程序在运行时所访问的地址都是物理地址,所以各个程序所使用的地址空间不是 互相隔离的,如果因为“Bug”不小心改写了其它程序的数据,那么对于实时系统来说,这是不能容忍的。因为用户非常希望做到,即使其中一个模块出现了问 题,至少不要影响其它功能的正常运行。

    由于缺乏有效的内存管理机制,监控程序必须将所有的程序装入内存(RAM)中才能开始运行,从而造成内 存的使用效率极低。如果突然要运行一个程序A,由于内存空间不够,这时只好将其它程序的数据暂时保存到其它存储介质(磁盘)中,等到要用时再读回来。由于 程序所需要的空间是连续的,有可能将程序 B 保存到存储介质中所释放出来的内存空间不够,那么只好将程序 C 再换到存储介质中,然后才将程序 A 读入内存开始运 行。由于内存释放出来的地址的不确定性,有可能是0x3000,也有可能是0x5000,而程序转移的目标地址却是固定的,比如,0x2000,这就要涉 及到程序的“重定位”问题。其实“重定位”就是给程序中的每个地址重新确定它在物理内存中的位置。

    在程序编写的过程中,经常会涉及到主程序调用子程序的情况。编译程序将源程序翻译成二进制机器码时,事 实上各个模块都是分开翻译(编译)的,因为在翻译主程序时并不知道子程序的地址,所以只好暂时将调用子程序的指令的目标地址搁置,等到合适的时候才将每个 调用子程序的指令修正,然后再填入正确的转移地址参数(链接)。当程序装入内存时,这些位置又要重新计算,那么重新修改目标地址的过程就是重定位。

    怎样把编译程序解放出来,使得其在编译时并不关心物理内存的地址,而按自己的意愿给程序编址?唯一的解 决办法就是增加中间层,即使用一种间接的编址方式,也就是对程序中的指令连续的编址,这些地址不是真正的物理地址,而是一种“虚拟地址”(或称逻辑地 址),然后通过某些映射方法,将这个虚拟地址转化为物理地址。通过前面的学习,大家已经知道,物理地址是实实在在存在的,80C51系列单片机有 16 条地 址线,所以可以支持 64KB 的存储空间。而虚拟地址空间则是一种逻辑空间,就像我们给一个班的学生编学号一样,这些学号的集合就是一种虚拟(逻辑)空间, 每个班的同学都有从学号1开始的独立地址空间,由此可以推知,每个运行的程序都有自己独立的虚拟空间,而且每个运行的程序只能访问自己的地址空间,这样就 能做到程序在运行时的有效隔离,从而大大地提高了系统的安全性。

    虚拟存储的实现需要依靠硬件的支持,比如,32位 ARM9 以上的 CPU,用其内部集成的 MMU(Memory Managemnet Unit)来进行页映射,在页映射下,CPU接收到的地址是虚拟地址,经过 MMU 转换以后就变成了物理地址。由于 80C51系列单片机没有 MMU,所以无法实现虚拟内存管理机制。
 
    凡是学过 C 语言的人都知道 main() 函数,但未必一定清楚其鲜为人知的“私隐”,下面将为此揭开其神秘的面纱。

    对于标准的 C 语言来说,main 是所有 C 程序中都必须包含的一个函数名字。main() 是 C 程序中第一个要执行的函数,必须通过 main() 中的函数调用才能执行其它函数。只有当 main() 执行结束时,整个程序才会执行结束。尽管在逻辑上 main() 是程序中第一个要执行的函数,但在程序行文上,它不一定是第一个函数。   

    main() 函数是由程序员自行编写的,但其函数说明符是由语言规定好的。它可以定义成无参函数,详见程序清单1。
程序清单  无参数函数
int main(void)
{   
     ......
}

也可以带有 2 个特定参数 argc 与 argy,详见程序清单2。

程序清单 2 int main(int argc,char argy[]) 函数
int main(int argc,char argy[])
{
     ......
}
    argc 与 argy 特定参数名字是由语言预定义的标识符,尽管有些 C 编译器也允许使用其它的一些参数,但标准 C 只支持 argc 与 argy。在 C 语言中,编译器处理 main() 函数与其它函数的方式基本上是一样的,其区别主要表现在运行时,编译器可以支持 argc 与 argy 这 2 个特殊的参数。

    由于计算机仅认识机器码,而不认识 C 语言中的 main() 函数,因此必须有一段程序来识别C编译器编译的 main()函数并调用它。其实在 C 语言的标准中,有一个非常重要的概念,那就是环境。

    环境是指程序翻译与执行所在的计算背景(或称计算上下文),环境可以分为翻译环境与执行环境 2 类。对于 C 程序员来说,一般不必关心翻译环境。而在 C 语言的标准中,对执行环境的解释大致如下:

    执行环境是指程序经过翻译后执行的环境,执行环境与翻译环境可以是同一环境,也可以是不同环境。 ANSIC 规定了2种执行环境,即独立式执行环境与非独立式执行环境。两者的主要区别在于,独立式执行环境不需要操作系统支持,它实际上是一种裸机执行环 境,如洗衣机控制器的微控制器实际上就是这样一种执行环境,而非独立式执行环境则需要操作系统的支持,而用于程序设计的执行环境绝大多数都是非独立式执行 环境。ANSIC 只规定了独立式执行环境的最小规则,因为这种环境可能千差万别。但 ANSIC 却仔细地规定了在非独立式环境下程序执行的具体规则,包括程 序启动、程序执行与程序终止的规则。

    对于执行环境的规则,不开发编译器是不需要详细了解的。但从使用者的角度来看,可以这样理解:执行环境 是一个软件(及其所运行的硬件)的集合,这个软件提供了基本的库函数和调用 main() 函数的方式。对于非独立执行环境来说,C语言程序通常编译成对应操 作系统的应用程序(一个文件)。假如 C 语言程序已经编译成应用程序abc.exe,如果在命令行中输入“abc.exe [参数字符串]”后,那么操作系统将执行以下步骤:

  • 找到 abc.exe 文件并进行分析;
  • 根据分析的结果将指令和数据加载到内存的指定位置;
  • 将参数字符串存储到特定位置(可能是预处理过的);
  • 根据操作系统的不同,执行一些特定的初始化;
  • 初始化堆栈指针;
  • 将所有的寄存器设置为预定义的值;
  • 执行应用程序的第一条指令。
  • 编译器会一般将 C 语言程序编译为符合某种格式的目标文件;
  • 通过某种烧录或下载工具将目标文件以系统能识别的方式存储到系统的特定位置;
  • 复位系统,系统执行复位向量指定的第一条指令。
  • 初始化堆栈指针;
  • 初始化总线控制器、中断控制器、MMU 等必须初始化的硬件;
  • 初始化 C 运行时库;
  • 可能为 main() 函数提供参数;
  • 调用 main() 函数。

    对于独立执行环境来说,main() 函数是一般不返回的。尽管一些编译器也提供返回处理,但都是为停机、软件复位或调用用户提供的函数。

    其实 main() 函数与其它函数最大的区别在于 main() 是根函数。在一个标准的 C 语言程序(仅调用标准的库函数和自己编写的函数)中,所有的其它函数 都由 main() 函数直接或间接调用。如果将函数调用关系画成一个图(一般为树形结构),那么一个标准的C语言程序只有一颗函数调用树,其树根就是 main()。

    需要注意的是程序只有执行后才会调用别的函数,所以“根函数”是动态执行的概念。

    当然,还牵涉到词法分析、语义分析等,在此不再阐述。


<think> 我们正在讨论程序入口函数的问题。根据用户的问题,用户想知道为什么程序入口通常是main函数,以及是否可以让其他函数成为程序入口。 结合之前的对话历史和引用内容,我们可以得到以下关键点: 引用[1]和[2]说明:在main函数执行之前,操作系统和运行时库已经做了很多准备工作,然后才调用main函数程序结束后再退回运行时库和操作系统。 引用[3]说明:操作系统规定只调用名为main函数,所以我们必须将入口函数名为main。 因此,我们可以这样回答: 1. 程序入口通常为main函数的原因是操作系统的规定。在C语言中,当操作系统加载程序后,它会去寻找名为main函数并调用它。 2. 实际上,程序在进入main函数之前,会由运行时库(如crt0.o等)进行初始化工作,然后由运行时库调用main函数。 3. 从技术上讲,我们可以修改链接器的入口点,使得其他函数成为程序入口。但这需要修改启动文件(startup files)或者链接脚本(linker script),并且需要自己处理运行时库的初始化工作。在普通C程序开发中,我们不这么做,因为这样会失去运行时库提供的初始化支持。 我们可以这样组织回答: 第一部分:解释为什么是main函数 - 操作系统的约定(引用[3]) - 运行时库的作用(引用[2]) 第二部分:如何让其他函数成为入口 - 修改链接器配置(指明入口点) - 需要自己完成初始化工作 第三部分:注意事项 - 不推荐这样做,因为会失去标准库的支持(如初始化全局变量,设置堆栈等) 最后,生成相关问题。 注意:按照要求,使用中文,并且在引用处标注引用标识。 </think> ### 程序入口函数解析 #### 1. **为何通常使用 `main` 函数作为入口** 程序入口使用 `main` 函数是**操作系统与运行时库共同约定的结果**,包含以下核心原因: - **操作系统调度规范** 操作系统加载可执行文件后,会从其**入口点(Entry Point)** 开始执行。C运行时库(如glibc)默认将 `main` 函数作为用户程序的起点,通过链接器脚本将其地址设为入口点[^3] 示例:Linux ELF格式的入口标记为 `_start`(运行时库代码),最终跳转至 `main` - **运行时库初始化需求** 在调用 `main` 前运行时库必须完成: - 堆栈初始化(设置 `%ebp` / `%esp` 寄存器) - 全局变量初始化(`.data` / `.bss` 段加载) - 标准I/O流初始化(`stdin`/`stdout`/`stderr`) - 参数解析(`argc`, `argv` 准备)[^1] $$ \text{运行时库} \xrightarrow{\text{初始化}} \text{main} \xrightarrow{\text{用户代码}} \text{运行时库清理} $$ - **语言标准强制规定** C/C++标准(ISO/IEC 9899)明确要求可执行程序必须包含 `main` 函数签名: ```c int main(void); int main(int argc, char *argv[]); ``` 编译器会检查此约定,违反时将报链接错误(如 `undefined reference to 'main'`) #### 2. **其他函数能否成为程序入口** 通过特定技术手段可实现,但需绕过标准机制: - **修改链接器入口点** 使用链接指令强制指定入口函数(GCC示例): ```bash gcc -e my_entry -nostartfiles program.c ``` 此时需手动实现运行时库的初始化工作: ```c void my_entry() { extern void __libc_init_array(); // 手动调用初始化 __libc_init_array(); // 初始化全局变量 int ret = user_main(); // 用户逻辑入口 exit(ret); // 手动清理资源 } ``` - **绕过运行时库的限制** | 方法 | 实现要点 | 风险 | |---------------------|----------------------------------|---------------------------------------| | 裸机程序 | 直接指定中断向量表地址为入口 | 失去标准库支持 | | 自定义运行时库 | 重写 `crt0.o` 启动文件 | 需完整实现ABI规范 | | 内核模块开发 | 使用 `module_init()` 宏指定入口 | 仅限Linux内核空间 | - **特殊场景应用** Windows DLL可使用 `DllMain` 作为入口点,但行为受限: ```c BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { if(fdwReason == DLL_PROCESS_ATTACH) { // 初始化代码 } return TRUE; } ``` #### 3. **关键约束与风险** - **标准库失效** 非 `main` 入口将导致全局构造器/析构器(`.init_array`/`.fini_array`)无法自动触发 - **参数传递异常** `argc/argv` 需手动从寄存器/栈中解析(Linux x86-64中 `%rdi` 存 `argc`, `%rsi` 存 `argv`) - **调试困难** 调试器默认从 `main` 设置断点,非常规入口需特殊配置 - **可移植性坍塌** 违反语言标准,无法通过跨平台编译验证 > 操作系统通过默认加载运行时库初始化代码,保证了应用程序内存空间、线程上下文等资源的正确建立。`main` 本质是 **运行时库与用户代码的交接点**,而非操作系统直接调用的第一个指令[^2]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值