C语言开发总结(十三)

C语言中没有main函数生成可执行程序的几种方法

1、define预处理指令
这种方式很简单,只是简单地将main字符串用宏来代替,或者使用##拼接字符串。示例程序如下:
#include <stdio.h>

#define begin main

int begin(void)
{
printf("Hello, World!\n");
return 0;
}


#include <stdio.h>

#define begin m##a##i##n

int begin(void)
{
printf("Hello, World!\n");
return 0;
}
严格来说,这种方式只算是一种技巧......
2、_start函数
_start函数是C程序的入口函数,会调用main函数。在调用main函数之前,会先执行_start函数分配必要的资源,然后再调用main函数。但是在用gcc编译程序时可以使用-nostartfiles选项来重写_start函数。示例程序如下:
#include <stdio.h>
#include <stdlib.h>

_start(void) {
printf("Hello, World!\n");
exit(0);
}
编译上面的程序的命令为:
gcc -nostartfiles _start.c -o a.out
反汇编生成的可执行程序,如下所示:
a.out: file format elf64-x86-64


Disassembly of section .plt:

0000000000400320 <puts@plt-0x10>:
400320: ff 35 ea 01 20 00 pushq 0x2001ea(%rip) # 600510 <_GLOBAL_OFFSET_TABLE_+0x8>
400326: ff 25 ec 01 20 00 jmpq *0x2001ec(%rip) # 600518 <_GLOBAL_OFFSET_TABLE_+0x10>
40032c: 0f 1f 40 00 nopl 0x0(%rax)

0000000000400330 <puts@plt>:
400330: ff 25 ea 01 20 00 jmpq *0x2001ea(%rip) # 600520 <_GLOBAL_OFFSET_TABLE_+0x18>
400336: 68 00 00 00 00 pushq $0x0
40033b: e9 e0 ff ff ff jmpq 400320 <puts@plt-0x10>

0000000000400340 <exit@plt>:
400340: ff 25 e2 01 20 00 jmpq *0x2001e2(%rip) # 600528 <_GLOBAL_OFFSET_TABLE_+0x20>
400346: 68 01 00 00 00 pushq $0x1
40034b: e9 d0 ff ff ff jmpq 400320 <puts@plt-0x10>

Disassembly of section .text:

0000000000400350 <_start>:
400350: 55 push %rbp
400351: 48 89 e5 mov %rsp,%rbp
400354: bf 68 03 40 00 mov $0x400368,%edi
400359: e8 d2 ff ff ff callq 400330 <puts@plt>
40035e: bf 00 00 00 00 mov $0x0,%edi
400363: e8 d8 ff ff ff callq 400340 exit@plt 
上面的结果是完整的反汇编结果,我们可以看到_start函数中只有我们调用printf和exit函数相关的一些指令,并且.txt段中只有_start函数,没有看到main函数。如果将源代码中的_start替换为main,重新编译程序,反汇编的结果中会看到_start函数会调用到main。
另外还有一点需要注意,因为这里重写了_start函数,所以gcc为默认的main函数准备的清理动作就没用上,所以如果退出的时候直接使用return,会导致程序崩溃。所以这里要使用exit()来退出程序。具体的原因可以参见这篇文章。
3、gcc的-e选项
示例程序如下:
#include <stdio.h>
#include <stdlib.h>

int nomain(int i, int j, int k) {
printf("Hello, World!\n");
exit(0);
}
将上面的程序保存为m.c,编译命令如下所示:
gcc -nostartfiles -e nomain m.c -o a.out
继续使用objdump反汇编生成的可执行程序,结果如下:
a.out: file format elf64-x86-64


Disassembly of section .plt:

0000000000400320 <puts@plt-0x10>:
400320: ff 35 f2 01 20 00 pushq 0x2001f2(%rip) # 600518 <_GLOBAL_OFFSET_TABLE_+0x8>
400326: ff 25 f4 01 20 00 jmpq *0x2001f4(%rip) # 600520 <_GLOBAL_OFFSET_TABLE_+0x10>
40032c: 0f 1f 40 00 nopl 0x0(%rax)

0000000000400330 <puts@plt>:
400330: ff 25 f2 01 20 00 jmpq *0x2001f2(%rip) # 600528 <_GLOBAL_OFFSET_TABLE_+0x18>
400336: 68 00 00 00 00 pushq $0x0
40033b: e9 e0 ff ff ff jmpq 400320 <puts@plt-0x10>

0000000000400340 <exit@plt>:
400340: ff 25 ea 01 20 00 jmpq *0x2001ea(%rip) # 600530 <_GLOBAL_OFFSET_TABLE_+0x20>
400346: 68 01 00 00 00 pushq $0x1
40034b: e9 d0 ff ff ff jmpq 400320 <puts@plt-0x10>

Disassembly of section .text:

0000000000400350 <nomain>:
400350: 55 push %rbp
400351: 48 89 e5 mov %rsp,%rbp
400354: 48 83 ec 10 sub $0x10,%rsp
400358: 89 7d fc mov %edi,-0x4(%rbp)
40035b: 89 75 f8 mov %esi,-0x8(%rbp)
40035e: 89 55 f4 mov %edx,-0xc(%rbp)
400361: bf 75 03 40 00 mov $0x400375,%edi
400366: e8 c5 ff ff ff callq 400330 <puts@plt>
40036b: bf 00 00 00 00 mov $0x0,%edi
400370: e8 cb ff ff ff callq 400340 <exit@plt>
从上面我们可以看到指定的nomain函数位于.text段的开始位置,同样在函数结束的时候没有gcc为main函数准备的清理动作,所以在这里也只能使用exit()来退出程序,而不能使用return。
4、nostartfiles选项
前面已经多次使用了该选项,不过都是配合其他选项使用的,这个选项也可以单独使用,其含义为"Do not use the standard system startup files when linking"。
示例程序如下:
#include <stdio.h>
#include <stdlib.h>

void func() {
printf("I am func....\n");
}

int nomain1(int i, int j, int k) {
func();
printf("%s: Hello, World!\n", __func__);
exit(0);
}
上面的程序保存为k.c,然后使用下面的命令编译:
[root@CentOS_190 ~]# gcc -nostartfiles p.c
/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400398
在单独使用nostartfiles选项时会报警告,生成的可执行程序可以执行,但是会产生段错误,去掉对func()函数的调用就不会产生段错误了。将生成的可执行程序反汇编,和使用前面的方法生成可执行程序的反汇编结果比较,发现除了函数名不一样外,没有其他区别,不知道为什么会产生段错误。知道的麻烦告知一声,拜谢!

相关阅读:

C语言变长数组之剖析 http://www.linuxidc.com/Linux/2013-07/86997.htm

C语言需要注意的问题 http://www.linuxidc.com/Linux/2013-05/84301.htm

C语言位域的使用及其注意点 http://www.linuxidc.com/Linux/2013-07/87027.htm


 

C 语言main 函数终极探秘



所有的C程序必须定义一个称之为main的外部函数,这个函数是程序的入口,也就是当程序启动时所执行的第一个函数,当这个函数返回时,程序也将终止,并且这个函数的返回值被看成是程序成功或失败的标志,如果在到达main函数体的末尾时没有遇到返回语句 ,它就被看看成是执行了return 0; 语句。

        C的设计原则是把函数作为程序的构成模块。main()函数称之为主函数,一个C程序总是从main()函数开始执行的

        在最新的C99中,main函数只有两种标准形式:

       int  main(void )  {  }

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

       在C99之前,main函数返回类型常常被省略,采用默认类型 int,其实,如果没有返回值, 编译器会给你加上。

      老版本的C代码,将会发现程序常常以

       main()
   
   这种形式开始。C90标准允许这种形式,但是C99标准不允许。因此即使你当前的编译器允许,也不要这么写。
    
     你还可能看到过另一种形式:

      void main()    
    有些编译器允许这种形式,但是还没有任何标准考虑接受它。C++ 之父 Bjarne Stroustrup 在他的主页上的 FAQ 中明确地表示:void main( ) 的定义从来就不存在于 C++ 或者 C 。所以,编译器不必接受这种形式,并且很多编译器也不允许这么写,这个形式只是编译器扩展的,并不是属于C标准。。
    
    坚持使用标准的意义在于:当你把程序从一个编译器移到另一个编译器时,照样能正常运行。

   返回值:

     由于是 int main( ..) 那么当时 应当返回 int 但是  return 2.3 ;也能运行正确,这是因为编译器自动转换2.3为int,截断后为return 2;  

     如果写为 return "abc";那么会报错,  error C2440: “return”: 无法从“const char [4]”转换为“int”

     C语言有两个宏

     #define EXIT_SUCCESS    0
     #define EXIT_FAILURE       1

     默认EXIT_SUCCESS 为成功返回, EXIT_FAILURE 为出错返回,相当于abort();
返回值的意义:

    return返回的数值由程序的作者自定。返回不同的值可以代表不同的含义,一般是代表出错的原因。传统上返回0代表程序正常结束(其它返回值代表什么含义,需要程序的开发者向程序的用户说明)。
在UNIX中,一个程序仅仅完成一个简单但有用的操作;不像Windows中的应用程序那样试图包办一切。所以,UNIX中很多程序都是可以分工协作的。后面程序的输入可以从前面程序的输出获得。这样在一些关键应用中,后面的程序可以检测一下前面的程序是否正常退出,如果是正常退出的,再按照预先的流程进行下面的操作;如果前面的程序不是正常退出,那么前面的程序的输出很可能不是后面程序需要的输入数据,这就需要进行特殊的处理。
而后面的程序就是靠前面程序main函数的返回值判断的。程序结束时,把值交给操作系统,然后后面的程序或者shell可以从操作系统中取得这个值。

      以下example 转自编程中国:

====================================================================================

      从前面我们知道main()函数的返回值类型是int型的,而程序最后的 return 0; 正与之遥相呼应,0就是main()函数的返回值。那么这个0返回到那里呢?返回给操作系统,表示程序正常退出。因为return语句通常写在程序的最后,不管返回什么值,只要到达这一步,说明程序已经运行完毕。而return的作用不仅在于返回一个值,还在于结束函数。
    现在我们来做一个小试验(注意:本人的系统是Windows XP, 编译环境是TC)来观察main()函数的返回值。编写如下代码并编译运行:

   //a.c    
    #include "stdio.h"
    int main(void)
    {
        printf("I love you.");
    
        return 0;
    }

    将这个文件保存为a.c,编译运行后会生成一个a.exe文件。现在打开命令提示符,在命令行里运行刚才编译好的可执行文件,然后输入   echo %ERRORLEVEL% ,回车,就可以看到程序返回 一个0 。如果把 return 0; 改为 return 99; ,那么很显然,再次执行上述步骤以后你可以看到程序返回99。要是你这样写 return 99.99; 那还是返回99,因为99.99被传给操作系统之前,被强制类型转换成整数类型了。
    现在,我们把a.c改回原来的代码,然后再编写另一个程序b.c:

    //b.c
    #include "stdio.h"
    int main(void)
    {
        printf("\nI'm too.");
    
        return 0; 
    }

    编译运行后打开命令提示符,在命令行里输入a&&b 回车,这样你就可以看到《人鬼情未了》里面经典的爱情对白:
    
    
I love you.
    I'm too.

    && 的含义是:如果 && 前面的程序正常退出,则继续执行 && 后面的程序,否则不执行。所以,要是把a.c里面的 return 0; 删除或者改为 return 99; ,那么你只能看到 I love you. 。也就是说,程序b.c就不执行了。现在,大家该明白 return 0; 的作用了吧。
从前面我们知道main()函数的返回值类型是int型的,而程序最后的 return 0; 正与之遥相呼应,0就是main()函数的返回值。那么这个0返回到那里呢?返回给操作系统,表示程序正常退出。因为return语句通常写在程序的最后,不管返回什么值,只要到达这一步,说明程序已经运行完毕。而return的作用不仅在于返回一个值,还在于结束函数。
    现在我们来做一个小试验(注意:本人的系统是Windows XP, 编译环境是TC)来观察main()函数的返回值。编写如下代码并编译运行:

   //a.c    
    #include "stdio.h"
    int main(void)
    {
        printf("I love you.");
    
        return 0;
    }

    将这个文件保存为a.c,编译运行后会生成一个a.exe文件。现在打开命令提示符,在命令行里运行刚才编译好的可执行文件,然后输入   echo %ERRORLEVEL% ,回车,就可以看到程序返回 一个0 。如果把 return 0; 改为 return 99; ,那么很显然,再次执行上述步骤以后你可以看到程序返回99。要是你这样写 return 99.99; 那还是返回99,因为99.99被传给操作系统之前,被强制类型转换成整数类型了。
    现在,我们把a.c改回原来的代码,然后再编写另一个程序b.c:

    //b.c
    #include "stdio.h"
    int main(void)
    {
        printf("\nI'm too.");
    
        return 0; 
    }

    编译运行后打开命令提示符,在命令行里输入a&&b 回车,这样你就可以看到《人鬼情未了》里面经典的爱情对白:
    
    
I love you.
    I'm too.

    && 的含义是:如果 && 前面的程序正常退出,则继续执行 && 后面的程序,否则不执行。所以,要是把a.c里面的 return 0; 删除或者改为 return 99; ,那么你只能看到 I love you. 。也就是说,程序b.c就不执行了。现在,大家该明白 return 0; 的作用了吧。

===================================================================================

  main函数参数:

     int main(void)  {  } 

     未声明任何参数 ,不会从环境向main函数传递任何信息,不过可以使用想getnev或system这样的库函数获取此类信息。

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

    声明的这些参数是由执行环境所创建的(执行环境在下面讨论),而不是在C语言的直接控制下,argc参数表示当用户或其他程序调用这个程序时传递给它的 “程序的参数”或 “选项”的数量,argv 参数是个指针向量,其中每个字符串指针分别表示传递给程序的参数, 第一个字符串argv[0]是程序的名称,包含路径,如果这个名称不可用,则argv[0]必须是 '\0',字符串arav[i](i=1,...argc-1)表式第 i 个程序参数,标准C 要求argv[argc]是个null指针,但在有些旧时编译器中却不是这样的,argv向量以及它所指向的字符串必须是可以修改的,并且他们的值在程序执行期间不能被编译器或操作系统所修改。如果编译器并不允许大小写混合的字符串 ,则存储在argv中的字符串必须采用小写形式。

    独立性C编译器以及一些软件框架(比如Microsoft Windows MFC) 对C程序的启动可能采用一些特殊的约定。

    当然argc ,argc形参名可以任意修改 ,只要不改变类型都可以。

     这个小程序打印出它的名称和参数:

      #include <stdio.h>

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

      {

         int i;

         printf("Name : %s\n",argv[0]);

         for(i=1 ; i<argc; i++)

           printf("%s \t",argv[i]);

         return 0;

      }

      运行程序后生成 debug.exe 文件,比如路径为 C:\ debug.exe

       打开cmd 输入 c:\debuge.exe I love you

       那么debug.exe 会运行,并在屏幕上打印出:

       C:\debuge.exe 

       I love you

-----------------------------------------------------------------------------------------------------------------------------------

   main函数之前的执行环境初始化,来自百度空间的文章:

       

main函数之前--真正的函数执行入口或开始

main函数之前--真正的函数执行入口或开始

一种解释

实际上,在可执行文件被加载之后,控制权立即交给由编译器插入的Start函数,它将对后面这些全局变量进行准备:   _osver 操作系统的构件编号      _winmajor 操作系统的主版本号      _winminor 操作系统的次版本号      _winver 操作系统完全版本号      __argc 命令行参数个数      __argv 指向参数字符串的指针数组      _environ 指向环境变量字符串的指针数组Start函数初始化堆并调用main函数.mian函数返回之后,Start函数调用Exit函数结束该进程.启动函数Start的源代码在:   crt0.c Microsoft Visual C++

      c0w.asm Borladn C++

另一种解释

Some of the stuff that has to happen before main():
set up initial stack pointer 
initialize static and global data 
zero out uninitialized data 
run global constructors
Some of this comes with the runtime library's crt0.o file or its __start() function. Some of it you need to do yourself.

Crt0 is a synonym for the C runtime library.
Depending on the system you're using the follwing may be incomplete, but it should give you an idea. Using newlib-1.9.0/libgloss/m68k/crt0.S as an outline, the steps are:
1. Set stack pointer to value of __STACK if set 
2. Set the initial value of the frame pointer 
3. Clear .bss (where all the values that start at zero go) 
4. Call indirect of hardware_init_hook if set to initialize hardware 
5. Call indirect of software_init_hook if set to initialize software 
6. Add __do_global_dtors and __FINI_SECTION__ to the atexit function so destructors and other cleanup functions are called when the program exits by either returning from main, or calling exit
7. setup the paramters for argc, argv, argp and call main 
8. call exit if main returns

第三种解释:囫囵C语言(三):谁调用了我的 main?
        
    我们都听说过一句话:“main是C语言的入口”。我至今不明白为什么这么说。就好像如果有人说:“挣钱是泡妞”,肯定无数砖头拍过来。这句话应该是“挣钱是泡妞的一个条件,只不过这个条件特别重要”。那么上面那句话应该是 “main是C语言中一个符号,只不过这个符号比较特别。”
    
    我们看下面的例子:
    
    /* file name test00.c */
    
    int main(int argc, char* argv)
    {
     return 0;
    }
    
    编译链接它:
    cc test00.c -o test.exe
    会生成 test.exe
    
    但是我们加上这个选项: -nostdlib (不链接标准库)
    cc test00.c -nostdlib -o test.exe
    链接器会报错:
    undefined symbol: __start
    
    也就是说:
    1. 编译器缺省是找 __start 符号,而不是 main
    2. __start 这个符号是程序的起始点
    3. main 是被标准库调用的一个符号
    
    再来思考一个问题:
    我们写程序,比如一个模块,通常要有 initialize 和 de-initialize,但是我们写 C 程序的时候为什么有些模块没有这两个过程么呢?比如我们程序从 main 开始就可以 malloc,free,但是我们在 main 里面却没有初始化堆。再比如在 main 里面可以直接 printf,可是我们并没有打开标准输出文件啊。(不知道什么是 stdin,stdout,stderr 以及 printf 和 stdout 关系的群众请先看看 C 语言中文件的概念)。
    
    有人说,这些东西不需要初始化。如果您真得这么想,请您不要再往下看了,我个人认为计算机软件不适合您。
    
    聪明的人民群众会想,一定是在 main 之前干了些什么。使这些函数可以直接调用而不用初始化。通常,我们会在编译器的环境中找到一个名字类似于 crt0.o 的文件,这个文件中包含了我们刚才所说的 __start 符号。(crt 大概是 C Runtime 的缩写,请大家帮助确认一下。)
    
    那么真正的 crt0.s 是什么样子呢?下面我们给出部分伪代码:
    
    ///////////////////////////////////////////////////////
    section .text:
    __start:
    
     :
     init stack;
     init heap;
     open stdin;
     open stdout;
     open stderr;
     :
     push argv;
     push argc;
     call _main; (调用 main)
     :
     destory heap;
     close stdin;
     close stdout;
     close stderr;
     :
     call __exit;

    ////////////////////////////////////////////////////
    
    实际上可能还有很多初始化工作,因为都是和操作系统相关的,笔者就不一一列出了。
    
    注意:
    1. 不同的编译器,不一定缺省得符号都是 __start。
    2. 汇编里面的 _main 就是 C 语言里面的 main,是因为汇编器和C编译器对符号的命名有差异(通常是差一个下划线'_')。
    3. 目前操作系统结构有两个主要的分支:微内核和宏内核。微内核的优点是,结构清晰,简单,内核组件较少,便于维护;缺点是,进程间通信较多,程序频繁进出内核,效率较低。宏内核正好相反。我说这个是什么目的是:没办法保证每个组件都在用户空间(标准库函数)中初始化,有些组件确实可能不要初始化,操作系统在创建进程的时候在内核空间做的。这依赖于操作系统的具体实现,比如堆,宏内核结构可能在内核初始化,微内核结构在用户空间;即使同样是微内核,这个东东也可能会被拿到内核空间初始化。
    
    随着 CPU 技术的发展,存储量的迅速扩展,代码复杂程度的增加,微内核被越来越多的采用。你会为了 10% 的效率使代码复杂度增加么?要知道每隔 18 个月 CPU 的速度就会翻一番。所以我对程序员的要求是,我首先不要你的代码效率高,我首先要你的代码能让 80% 的人迅速看懂并可以维护。

总结:

main函数执行之前,主要就是初始化系统相关资源:

1.设置栈指针

2.初始化static静态和global全局变量,即data段的内容

3.将未初始化部分的赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL,等等,即.bss段的内容

4.运行全局构造器,估计是C++中构造函数之类的吧

5.将main函数的参数,argc,argv等传递给main函数,然后才真正运行main函数


三、main()函数的参数

C编译器允许main()函数没有参数,或者有两个参数(有些实现允许更多的参数,但这只是对标准的扩展)。这两个参数,一个是int类型,一个是字符串类型。第一个参数是命令行中的字符串数。按照惯例(但不是必须的),这个int参数被称为argc(argument count)。大家或许现在才明白这个形参为什么要取这么个奇怪的名字吧,呵呵!至于英文的意思,自己查字典吧。第二个参数是一个指向字符串的指针数组。命令行中的每个字符串被存储到内存中,并且分配一个指针指向它。按照惯例,这个指针数组被称为argv(argument value)。系统使用空格把各个字符串格开。一般情况下,把程序本身的名字赋值给argv[0],接着,把最后的第一个字符串赋给argv[1],等等。

现在我们来看一个例子:

//c.c
#include "stdio.h"
int main(int argc, char *argv[])
{
    int count;
    printf("The command line has %d arguments:
", argc - 1);
    for(count = 1; count < argc; count++)
        printf("%d: %s
", count, argv[count]);
    return 0;
}
编译运行,在命令行输入c I love you 回车,下面是从命令行运行该程序的结果:

The command line has 3 arguments:

1:I

2:love

3:you

从本例可以看出,程序从命令行中接受到4个字符串(包括程序名),并将它们存放在字符串数组中,其对应关系:

argv[0]  ------>    c(程序名)

argv[1]  ------>    I

argv[2]  ------>    love

argv[3]  ------>    you

至于argc的值,也即是参数的个数,程序在运行时会自动统计,不必我们操心。

这个例子中,每个字符串都时一个单词(字母),那既然是字符串,要把一句话当作参数赋给程序该怎么办?你可以在命令行里这样输入 c  "I love you."  "I’m too."。程序运行结果:

The command line has 2 arguments:

1:I love you.

2:I’m too.

其对应关系:

argv[0]  ------>    c(程序名)

argv[1]  ------>    I love you.

argv[2]  ------>    I’m too.

要注意的是,你在命令行的输入都将作为字符串形式存储于内存中。也就是说,如果你输入一个数字,那么要输出这个数字,你应该用%s格式而非%d或者其他。

再来看一个例子:

//d.c
#include "stdio.h"
int main(int argc, char *argv[])
{
    FILE *fp;
    fp = fopen(argv[1], "w");
    fputs("I love you.", fp);
    fclose(fp);
    return 0;
}
编译运行,打开命令行并输入d love.txt 回车。这样,打开d.c文件所在的目录,你会发现多了一个名为 love.txt 的文件,打开后里面的内容正是世界上说的最多的那句话。

当然,你可能会说我不需要使用命令行参数就可以做到这些。是的,你当然可以。使用命令行参数的理由或许就是练习命令行用法,以备以后需要编写基于命令行的程序。还有一个好处是,不需要C环境就可以运行已经编译好的程序。比如,你把上面那个程序编译后生成的d.exe发给你的女朋友,再告诉她怎么运行,这样,你的女朋友可以用另一种方式体会到你对她的浓情蜜意。

你可能感兴趣的:(C语言开发总结(十三))