发生在main函数之前的故事(C语言)

首先申明,此main 函数,特指C语言所编译得到的可执行文件入口函数。


         其实很多人在编程实践中都不免和main函数之前执行的代码相关概念打交道,诸如命令行参数,当前工作目录之流的。事实上,对于这个部分的讨论很多,笔者也阅读过不少,不过感觉往往都视角过窄,未必能观全貌,故有此文。

         

        首先这个问题的答案和C/C++标准几乎没有什么关系,标准对此问题所作的规定,非常少,下文内容中如果除非专门提到,否则都不属于C/C++标准文档内容。


         讨论这个问题的“入口”, 笔者认为再也没有比从嵌入式开发的bootloader开发说起更合适的。Yes,刚刚开机,一切都处于混沌的,原始的状态。 兄弟你知道一个2进制的文件,想要执行,最低限度的必须条件是什么吗?

         首先要把这段二进制数据加载到可以随机访问的存储器中,这个没有疑问把,哪怕是汇编语言所编译出来的二进制文件也不能免俗。有时候,需要用特殊的方法把某些代码烧到CPU上电之后,指令寄存器所指向的存储区域。比方说arm好像就是0x000000000,这样CPU开机就可以执行我们特定的代码。当然这些地址,对应地址线连接的都应该是非易失存储器,如FLASH之流。大家顶可把他们想成内存。

        汇编执行没有问题了,现在我们想让汇编语言调用C语言代码,让整个系统进入“C语言的世界”,还需要什么工作呢?一个跳转指令就行了吗?NO.   C语言的基本代码单位是函数,而绝大多数现代C语言编译器往往把函数参数以及局部变量,右值等,存储在内存中(数量少的就临时压到空闲的寄存器里去)。这些随时可能需要发生的内存分配请求必须要有一个仓库可以随时满足(请注意全局变量和静态变量以及字符串常量所消耗的内存空间属于一次分配)。YES, 这个仓库的名字叫做堆栈,我们需要初始化栈寄存器之类的寄存器。这样C语言的代码编译出来的二进制内容我们就可以访问了。可以参考linux内核的加载过程,先把2进制镜像加载或者解压到内存中,然后初始化栈顶(一般发行版都爱设置为0x30000000),最后再用汇编语句跳转指令跳转到某个函数的地址,OK. 

        好了,启动加载成功,我们已经进入了操作系统的内核世界。我们首先碰到的东西往往都和驱动相关,做过linux驱动的朋友都知道,linux的驱动不能够链接C库,甭管静态的还是动态的。什么原因呢?我们已经能用C语言了,为什么还不能用C库函数呢?呵呵,一来不能动态分配内存。注意动态内存分配服务的提供涉及到大量的内存管理服务的加载,包括泄露内存回收,内存碎片处理等等一大堆啰里啰唆的东西,想要用像malloc这样的函数以及依赖动态分配内存的函数的话,绝对有一大堆东西需要提前初始化,这太痛苦了,也太慢了吧。其次往往我们平时用的C库函数的release版都有一些针对用户线程的优化,而这些优化被用到了内核线程的执行的话,往往要出一些问题。最后标准输入输出流是什么呢?内核表示不和任何终端绑定(注意终端和控制台的区别),不懂stdio. OK, C运行时库定义了一个计算机模型,对于刚刚启动的内核来说还是太复杂了,一般都只能让用户进程启动的时候才把这些库函数载入内存,内核就对他们说拜拜了。

      该见到一个让你输入密码的东西了吧?输完密码,甭管你看到的是GUI桌面还是黑白色的简陋终端,这个时候我们终于可以自由自在的启动一个程序了。马上就可以看到我们的main函数了,千万别着急。

     等等,程序是什么,文件?我们不是说过只有载入内存才能执行吗?OK,一般的操作系统都给咱们封装了这个操作了,靠的是一个叫SHELL的东东。请注意你在windows或者gtk桌面点击一个可执行程序的时候,SHELL依然在发挥自己的作用。一般说来SHELL的作用包括:

      1)传递和组织命令行参数到内存中去。main 函数的参数不是白来的,run.exe "1 2 3 4" 5,  输入参数中对空白和引号的解析,以及在字符串尾部加一个终止符等操作,一般都发生在shell这一个过程。请注意windows的可以通过快捷方式配置命令行参数。

      2)把可执行文件载入内存。 说了好多遍了,不说了。

      3)创建进程,按照可执行文件中编译时的一些参数,设置进程参数。如默认优先级,初始线程栈大小之类的。

      4)分析动态库依赖,映射对应的动态库到创建好的进程的内存空间中去,然后根据那些动态库函数的地址,重新定位程序中一些“待定项”的数值。

      5)拷贝当前工作路径和环境变量键值对等信息到进程空间。

      以上内容属于各个操作系统中比较通用的部分,不通用的部分俺就不说了,接下来还要做一些事情。第一我们需要先给一些特殊的全局变量赋初值。 const char *str="12345678"; str的值这个时候就得按照"12345678"的当前地址给补上(和你的操作系统的进程模型和编译器有关,在某些情况下有可能这一步直接在链接的时候给搞定了);第二静态变量初值是0,一般都统一放到一块空间,把这块空间置0。接下来:调用某个C函数。

       哪个C函数,不是main函数吗?NO. 

       我们通常编写的程序都链接了C库,而很多C库函数使用之前都需要做一些初始化操作。比方说errno 需要设置成为一个无害的值,比方说malloc要能够服务大众,往往还需要做一些准备工作,比方说linux  的glibc通过SBRK MMAP等系统调用操作来帮着初始化一个内存池之类的。。。(够啰嗦了吧)。。。所以一般调用的都是一个叫做maincrt之类的函数(注意这个函数往往不属于C库,各个SDK不一样。linux这个函数在libcrt0.o,或者libcrt1.o中,而windows vs带的sdk这个函数一般在crt.obj中.编译器默认会自动给我们链接的),这个函数啰里啰唆的做完一大堆的准备工作,终于,

        “SHELL 传进来的参数在哪里?放在0x12345678啊,行: 调用main(3,0x12345678)

                                                                                                                                -----------(待续) 

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值