main函数

来源:互联网 发布:sql server error 编辑:程序博客网 时间:2024/06/06 01:00

C++

(1)   

之前在调试exe时感觉很奇怪,为什么Entry Point并非直接进入到main函数。

举例来说,如果将一段空的C代码build为exe:

C代码  收藏代码
  1. void main(){ }  

编译环境为:VC6 release。

再将该exe文件进行反汇编,那么从EP开始的代码部分大概形如:

ASM代码
push ebp
……
一段汇编代码 
……
call Main函数 
……
另一段汇编代码 
……
retn

 

也就是说, 在执行一个exe文件时,总是要先运行一些指令,才能够开始调用Main函数。同样,当main函数执行完毕后,还需要运行一些指令完成收尾。为了弄清楚main函数调用前这些代码以及main函数执行后的代码,需要从CRT(C RunTime ,C的运行时库)开始研究。

 

(2)

Visual Studio自带了CRT的源码,VC6中CRT位于“VC98\CRT\SRC”目录。CRT 中的 crt0.c 文件规定了一整套C程序固定的执行流程。在 crt0.c 开头的注释部分有如下描述:

This the actual startup routine for apps. It calls the user's main routine [w]main() or [w]WinMain after performing C Run-Time Library initialization.

大概意思是,当C Run-Time Library 完成了初始化工作之后,才开始执行用户自定义的main 函数、WinMain 函数。

 

crt0.c 为了规定C程序执行的流程,定义了函数mainCRTStartup 和WinMainCRTStartup ,这两个函数也被称作启动函数 。它们的作用在函数注释中已经写的很清楚:

This routine does the C runtime initialization, calls main(), and then exits.

 

mainCRTStartup函数大概形如:

C代码  收藏代码
  1. void mainCRTStartup(void){  
  2.     int mainret;  
  3.     ……  
  4.     __try {  
  5.         ……  
  6.         mainret = main(__argc, __argv, _environ); //在这里调用用户写的main函数   
  7.         exit(mainret);  
  8.     }  
  9.     __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) )  
  10.     {  
  11.         _exit( GetExceptionCode() );  
  12.     }  
  13. }  

当Windows系统执行一个C程序时,真正首先执行的是(win)mainCRTStartup函数。mainCRTStartup首先进行了一系列准备工作,例如heap的初始化、IO的初始化、获得命令行参数等等。当所有的准备工作都完成之后,再去调用用户自定义的main函数。最后,执行exit函数退出程序。因此对于exe,(win)mainCRTStartup函数才是真正的Entry point。

 

另外,crt0.c 中还有相似的函数:wWinMainCRTStartup、wmainCRTStartup,它们是Unicode版本程序的EP,这里可以暂时不用去管。windows为了照顾Unicode程序,很多API都提供了两种版本,一种是针对ANSI字符,还有一种是针对Unicode字符。

 

这四个函数是放在一起定义的,crt0.c 中的源码如下:

Cpp代码  收藏代码
  1. #ifdef _WINMAIN_                   /* _WINMAIN_被定义时,表示GUI程序 */  
  2.   
  3. #ifdef WPRFLAG                     /* WPRFLAG被定义时,表示Unicode字符 */  
  4. void wWinMainCRTStartup(  
  5. #else   
  6. void WinMainCRTStartup(  
  7. #endif  
  8.   
  9. #else                              /* 下面为CUI程序 */  
  10.   
  11. #ifdef WPRFLAG  
  12. void wmainCRTStartup(  
  13. #else   
  14. void mainCRTStartup(  
  15. #endif   
  16.   
  17. #endif  
  18.   
  19. void){  
  20. ……  
  21. }  

可以根据上面的源代码总结如下:

mainCRTStartupConsole appsANSIwmainCRTStartupConsole appsUnicodeWinMainCRTStartupWindows appsANSIwWinMainCRTStartupWindows appsUnicode

 

(3)

来具体看一下(win)mainCRTStartup函数。下面将mainCRTStartup函数的主要语句摘录了出来,这里去除了一些条件编译的代码,忽略了windows apps(_WINMAIN_)、Unicode版本的程序(WPRFLAG)、多线程(_MT),仅仅分析Console apps。

C代码  收藏代码
  1. int mainret;  
  2.   
  3. // 获取Win32的版本  
  4. _osver = GetVersion();  
  5. _winminor = (_osver >> 8) & 0x00FF ;  
  6. _winmajor = _osver & 0x00FF ;  
  7. _winver = (_winmajor << 8) + _winminor;  
  8. _osver = (_osver >> 16) & 0x00FFFF ;  
  9.   
  10. // 创建了一个属于该进程的私有堆  
  11. if ( !_heap_init(0) )  
  12.     fast_error_exit(_RT_HEAPINIT);  
  13.   
  14.   
  15. __try {  
  16.     // 初始化低级IO  
  17.     _ioinit();  
  18.   
  19.     // 获取命令行缓冲区指针  
  20.     _acmdln = (char *)GetCommandLineA();  
  21.   
  22.     // 获取环境变量指针  
  23.     _aenvptr = (char *)__crtGetEnvironmentStringsA();  
  24.       
  25.     // 设置argv参数  
  26.     _setargv();  
  27.   
  28.     // 设置环境变量  
  29.     _setenvp();  
  30.   
  31.     // 初始化C数据  
  32.     _cinit();  
  33.   
  34.     __initenv = _environ;  
  35.   
  36.     // 调用main函数  
  37.     mainret = main(__argc, __argv, _environ);  
  38.     exit(mainret);  
  39. }  
  40. __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) )  
  41. {  
  42.     _exit( GetExceptionCode() );  
  43. }  

 

从这段代码可以大体窥视出 C 程序的启动流程:

1.获取WIN32平台的版本

2.调用_heap_init函数创建一个私有堆

3.初始化低级IO
4.获取命令行缓冲区、环境变量的指针

5.设置命令行参数与环境变量

6.初始化C数据

7.调用main函数

8.将main函数的调用结果传入exit 退出程序

GetVersion

GetVersion函数是kernel32.dll中提供的API,用于获取当前Win平台的版本。准确的说,GetVersion可以获得3个信息:

1. OSPlatformId

2. OSBuildNumber

3. OSMinorVersion

4. OSMajorVersion

其中比较诡异的是OSPlatformId,在GetVersion的过程当中它被获得过,但是返回的时候又没了...所以只剩下2、3、4

 

可以用OD来跟进GetVersion:

Asm代码  收藏代码
  1. mov     eax, dword ptr fs:[18]  
  2. mov     ecx, dword ptr [eax+30]  
  3. mov     eax, dword ptr [ecx+B0]        // 获取 OSPlatformId  
  4. movzx   edx, word ptr [ecx+AC]         // 获取 OSBuildNumber   
  5. xor     eax, FFFFFFFE  
  6. shl     eax, 0E                        // 几次左移,把OSPlatformId的信息给移没了...  
  7. or      eax, edx  
  8. shl     eax, 8  
  9. or      eax, dword ptr [ecx+A8]        // 获取 OSMinorVersion  
  10. shl     eax, 8  
  11. or      eax, dword ptr [ecx+A4]        // 获取 OSMajorVersion  
  12. retn  

GetVersion主要是去PEB(Process Environment Block)结构中访问当前的OS信息,每个进程都会有自己独立的PEB。想要获取当前进程的PEB地址,首先要先访问TEB(Thread Environment Block )结构。因为TEB结构的30偏移量处中存放了PEB结构的指针。FS 寄存器指向了当前活动线程的TEB结构,其中偏移位置18表示了FS 段寄存器在内存中的镜像地址。

TEB结构
000  指向SEH链指针
004  线程堆栈顶部
008  线程堆栈底部
00C  SubSystemTib
010  FiberData
014  ArbitraryUserPointer
018  FS段寄存器在内存中的镜像地址
020  进程PID
024  线程ID
02C  指向线程局部存储指针
030  PEB结构地址(进程结构)
034  上个错误号

 

因此

mov     eax, dword ptr fs:[18]

mov     ecx, dword ptr [eax+30]

两句表示根据TEB获取PEB的指针,并且存放在ECX中。拿到PEB的指针后,就可以去PEB中拿OS的信息。

PEB结构中关于OS信息的偏移量如下:

PEB 写道
typedef struct _PEB // Size: 0x1D8
……
/*0A4*/ ULONG OSMajorVersion;
/*0A8*/ ULONG OSMinorVersion;
/*0AC*/ USHORT OSBuildNumber;
/*0AE*/ USHORT OSCSDVersion;
/*0B0*/ ULONG OSPlatformId;
……

 

如果是XP用户,那么GetVersion最终的返回值应该是:0A280105 。

其中 0A28表示XP build版本是2600,01表示OSMinorVersion,05表示OSMajorVersion 。

 

中间被忽略掉的 OSPlatformId 信息可以为:

  • VER_PLATFORM_WIN32s 或 0x0000,用于指定 Microsoft Windows 3.1。

  • VER_PLATFORM_WIN32_WINDOWS 或 0x0001,用于指定 Windows 95、Windows 98 或从其继承的操作系统。

  • VER_PLATFORM_WIN32_NT 或 0x0010,用于指定 Windows NT 或从其继承的操作系统。

另外,还有一些补充的OS信息如下:

(摘录自 http://msdn.microsoft.com/zh-cn/library/ms724833%28v=VS.85%29.aspx )

 

Operating systemVersion numberdwMajorVersiondwMinorVersionWindows 76.161Windows Server 2008 R26.161Windows Server 20086.060Windows Vista6.060Windows Server 2003 R25.252Windows Home Server5.252Windows Server 20035.252Windows XP Professional x64 Edition5.252Windows XP5.151Windows 20005.050------------- from http://driftcloudy.iteye.com/blog/1048750----------------------

Java


public static void main(String[] args) {}

但是以前一直都没有问自己,为什么要这么写,因为在c语言中就没有这样子的要求。其实这是一个不需要解释的问题,因为java标准就是这么规定的,那么既然是java标准规定的,我们按照规定来执行就好了。不过,这并不是一个很好的学习态度,如果总是知其然而不知其所以然,总会对java有种隔膜的感觉。就是发现问题了,不去解决,不去了解为什么,心里总是会有牵绊。今天既然自己都这么问自己了,为什么java的主函数要按照这个格式来写,那么我就得弄明白为什么。

在java中,main()方法是java应用程序的入口方法。java虚拟机通过main方法找到需要启动的运行程序,并且检查main函数所在类是否被java虚拟机装载。如果没有装载,那么就装载该类,并且装载所有相关的其他类。因此程序在运行的时候,第一个执行的方法就是main()方法。通常情况下, 如果要运行一个类的方法,必须首先实例化出来这个类的一个对象,然后通过"对象名.方法名()"的方式来运行方法,但是因为main是程序的入口,这时候还没有实例化对象,因此将main方法声明为static的,这样这个方法就可以直接通过“类名.方法名()”的方式来调用。

实例

 虚拟机通过调用某个指定类的方法main启动,传递给main一个字符串数组参数,使指定的类被装载,同时链接该类所使用的其它的类型,并且初始化它们。例如对于程序:

复制代码
public class HelloApp {    public static void main(String[] args) {        System.out.println("Hello World!");        for (int i = 0; i < args.length; i++) {            System.out.println(args);        }    }}
复制代码

 编译后在命令行模式下键入: java HelloApp run virtual machine

  将通过调用HelloApp的方法main来启动java虚拟机,传递给main一个包含三个字符串"run"、"virtual"、"machine"的数组。现在我们略述虚拟机在执行HelloApp时可能采取的步骤。

  开始试图执行类HelloApp的main方法,发现该类并没有被装载,也就是说虚拟机当前不包含该类的二进制代表,于是虚拟机使用ClassLoader试图寻找这样的二进制代表。如果这个进程失败,则抛出一个异常。类被装载后同时在main方法被调用之前,必须对类HelloApp与其它类型进行链接然后初始化。链接包含三个阶段:检验,准备和解析。检验检查被装载的主类的符号和语义,准备则创建类或接口的静态域以及把这些域初始化为标准的默认值,解析负责检查主类对其它类或接口的符号引用,在这一步它是可选的。类的初始化是对类中声明的静态初始化函数和静态域的初始化构造方法的执行。一个类在初始化之前它的父类必须被初始化。整个过程如下:



在Java中,main()方法是Java应用程序的入口方法,也就是说,程序在运行的时候,第一个执行的方法就是main()方法,这个方法和其他的方法有很大的不同,比如方法的名字必须是main,方法必须是public static void 类型的,方法必须接收一个字符串数组的参数等等。
 
在看Java中的main()方法之前,先看一个最简单的Java应用程序HelloWorld,我将通过这个例子说明Java类中main()方法的奥秘,程序的代码如下:
 
/** 
* Java中的main()方法详解 
*/
 
public class HelloWorld { 
    public static void main(String args[]) { 
        System.out.println("Hello World!"); 
    } 
}
 
一、先说类:
 
HelloWorld 类中有main()方法,说明这是个java应用程序,通过JVM直接启动运行的程序。
既然是类,java允许类不加public关键字约束,当然类的定义只能限制为public或者无限制关键字(默认的)。
 
二、再说main()方法
 
这个main()方法的声明为:public static void main(String args[])。必须这么定义,这是Java的规范。
 
为什么要这么定义,和JVM的运行有关系。
当一个类中有main()方法,执行命令“java 类名”则会启动虚拟机执行该类中的main方法。
 
由于JVM在运行这个Java应用程序的时候,首先会调用main方法,调用时不实例化这个类的对象,而是通过类名直接调用因此需要是限制为public static。
 
对于java中的main方法,jvm有限制,不能有返回值,因此返回值类型为void。
main方法中还有一个输入参数,类型为String[],这个也是java的规范,main()方法中必须有一个入参,类细必须String[],至于字符串数组的名字,这个是可以自己设定的,根据习惯,这个字符串数组的名字一般和sun java规范范例中mian参数名保持一致,取名为args。
 
因此,main()方法定义必须是:“public static void main(String 字符串数组参数名[])”。
 
三、main()方法中可以throw Exception
 
因此main()方法中可以抛出异常,main()方法上也可以声明抛出异常。
 
比如,下面这个写法是正确的:
public class TestMain { 
        public static void main(String[] args) throws Exception { 
                System.out.println("哈哈哈哈哈"); 
                throw new Exception(""); 
        } 
}
 
运行结果:
哈哈哈哈哈 
Exception in thread "main" java.lang.Exception:    
  at maintest.TestMain.main(TestMain.java:11) 
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) 
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) 
  at java.lang.reflect.Method.invoke(Method.java:585) 
  at com.intellij.rt.execution.application.AppMain.main(AppMain.java:90) 

Process finished with exit code 1
 
四、main()方法中字符串参数数组作用
 
main()方法中字符串参数数组作用是接收命令行输入参数的,命令行的参数之间用空格隔开。
 
下面给出一个例子,看看如何初始化和使用这个数组的。
/**
* 打印main方法中的输入参数 
*/
 
public class TestMain { 
    public static void main(String args[]){ 
        System.out.println("打印main方法中的输入参数!"); 
        for(int i=0;i<args.length;i++){ 
            System.out.println(args[i]); 
        } 
    } 
}
 
执行方法和运行结果
D:\Study\basetest\src>javac TestMain.java 

D:\Study\basetest\src>java TestMain 1 2 3 
打印main方法中的输入参数! 



 
 
五、给出HelloWorld的另外一个版本
 
/** 
* 变态版的HelloWorld.呵呵 
*/
 
public class HelloWorld2 { 
    static { 
        System.out.println("Hello Wordld!"); 
    } 
    public static void main(String args[]){ 
        System.exit(0); 
    } 
}
 
这个main()方法执行的内容就一句"System.exit(0);" ,目的是让程序正常结束。那“HelloWorld!”是从哪里打印的,秘密就是在static打印的,因为static代码块的内容会在main调用前调用。
 
总结:
main方法作为一个特殊的规范,与普通的方法有很大区别,限制很多,理解其原理需要学习JVM相关知识。
from:http://lavasoft.blog.51cto.com/62575/53263/

3 1