Windows核心编程之核心总结(第四章 进程(一))(2018.6.8)

学习目标

第四章进程的学习可谓是任重而道远,虽然不难,但知识量很多,也比较零散,需要多总结,脑海里才有进程的框架。所以,我把本章分为几个小节来讲完。我还是一如既往的添加辅助性内容,希望对于小白有所帮助。而比我流弊的大有人在,大神们可以跳过辅助性内容。本小节的学习目标如下:
1.C/C++程序编译过程
2.C/C++命令行参数的使用
3.什么是进程
4.Windows的入口点函数
5.进程实例句柄(可执行文件实例句柄或者DLL文件实例句柄)

C/C++程序编译过程

C/C++的编译、链接过程要把我们编写的一个c/c++程序(源代码)转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。编译就是把文本形式源代码翻译为机器语言形式的目标文件的过程。链接是把目标文件、操作系统的启动代码和用到的库文件进行组织形成最终生成可执行代码的过程。过程图解如下:
Windows核心编程之核心总结(第四章 进程(一))(2018.6.8)

C/C++的命令行

C/C++语言中的main函数,经常带有参数argc,argv,如下:

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

从函数参数的形式上看,包含一个整型和一个指针数组。当一个C/C++的源程序经过编译、链接后,会生成扩展名为.EXE的可执行文件,这是可以在操作系统下直接运行的文件,换句话说,就是由系统来启动运行的。对main()函数既然不能由其它函数调用和传递参数,就只能由系统在启动运行时传递参数了。在操作系统环境下,一条完整的运行命令应包括两部分:命令与相应的参数。其格式为:命令参数1参数2....参数n¿此格式也称为命令行。命令行中的命令就是可执行文件的文件名,其后所跟参数需用空格分隔,并为对命令的进一步补充,也即是传递给main()函数的参数。
命令行与main()函数的参数存在如下的关系:

设命令行为:program str1 str2 str3 str4 str5

其中program为文件名,也就是一个由program.c经编译、链接后生成的可执行文件program.exe,其后各跟5个参数。对main()函数来说,它的参数argc记录了命令行中命令与参数的个数,共6个,指针数组的大小由参数argc的值决定,即为char*argv[6],指针数组的取值情况如下图所示:
Windows核心编程之核心总结(第四章 进程(一))(2018.6.8)
数组的各指针分别指向一个字符串。应当引起注意的是接收到的指针数组的各指针是从命令行的开始接收的,首先接收到的是命令,其后才是参数。

什么是进程

(1)进程的概念
书中原文是这样写的:一个进程,就是一个正在运行的程序!一个程序,可以产生多个进程。
1.一个内核对象,被系统用来管理这个进程,这个内核对象中,还包含了进程的一些策略信息。
2.一个地址空间,这个地址空间中包含了可执行代码,动态链接库模块代码,数据,程序动态内存分配获取的内存,也在这个内存地址空间中。

在操作系统的相关书籍里是这样说的:由程序段、相关的数据段和PCB三部分构成进程,所以,其实程序段、相关的数据段就是一个地址空间,而PCB(进程控制块)就是内核对象。
(1) 进程和线程的关系
书中原文是这样写的:进程是由“惰性“的,进程要做任何事情都必须让一个线程在它的上下文中运行。该线程负责执行进程地址空间包含的代码。事实上,一个进程可以有多个线程,所有线程都在进程的地址空间中”同时执行代码“。…此处省略一些字...。每个进程至少要有一个线程来执行进程地址空间包含的代码。当系统创建一个进程的时候,会自动为进程创建第一个线程,这称为主线程。然后这个主线程再创建更多的线程,后者再创建更多的线程。单个CPU,为线程分配CPU采用循环方式,为每个线程都分配时间片;多个CPU,采取更复杂的算法为线程分配CPU。
怎么理解进程和线程的关系?举个例子就十分透彻了。当双击一个程序,产生了一个工厂(进程)同时也产生了第一个人----厂长(primary thread:主线程),这个厂长只做一件事就是招募(创建)员工(线程),让其他员工(线程)帮他做事。有两种方法工厂会倒闭(进程销毁),第一种是工厂里的员工(线程,包括主线程)全部退出或销毁,那么工厂自然会倒闭(进程销毁)。第二种方法是调用ExitProcess函数可以直接结束进程,第二种方法后面会讲到,现在先了解有这一方法结束进程即可。

Windows的入口点函数

Windows支持两种类型的应用程序:GUI程序(图形用户界面程序)和CUI程序(控制台用户界面程序)。当我们用Visual Studio来创建一个应用程序项目时,集成开发环境会设置各种链接器开关,使链接器将子系统的正确C/C++运行启动函数嵌入最终生成的可执行文件中。对于GUI程序,链接器开关是/SUBSYSTEM:CONSOLE;对于CUI程序,链接器开关是/SUBSYSTEM:WINDOWS。在学习C与C++时,当运行一个可执行文件,我们都认为系统调用的第一个函数是入口点函数(例如:main函数),但其实操作系统实际并不调用我们写的入口点函数(例如:main函数),实际最先调用的是C/C++运行库的启动函数。应用程序类型和相应的入口函数:

应用程序类型 入口点函数 嵌入可执行文件的启动函数
处理ANSI字符和字符串的GUI应用程序 _tWinMain (WinMain) WinMainCRTStartup
处理Unicode字符和字符串的GUI应用程序 _tWinMain (wWinMain) wWinMainCRTStartup
处理ANSI字符和字符串的CUI应用程序 _tmain (Main) mainCRTStartup
处理Unicode字符和字符串的CUI应用程序 _tmain (Wmain) wmainCRTStartup

要生成一个可执行文件,必须经过编译链接过程。当在链接成可执行文件时,如果系统发现该项目指定了/SUBSYSTEM:WINDOWS链接器开关,链接器就会在程序代码中寻找WinMain或wWinMain函数,如果没有找到这两个函数(要么入口点函数写成main或wmain函数或者没有写入口点函数),链接器将返回一个“unresolved external symbol“(无法解析的外部符号错误);如果找到了这两个函数,则根据具体情况(是Unicode字符集还是多字节字符集)选择WinMainCRTStartup或 wWinMainCRTStartup启动函数,再将启动函数嵌入到可执行文件中。类似地,如果系统发现该项目指定了/SUBSYSTEM:CONSOLE链接器开关,链接器就会在程序代码中寻找main或wmain函数,如果没有找到这两个函数(要么入口点函数写成WinMain或wWinMain函数或者没有写入口点函数),链接器将返回一个“unresolved external symbol“(无法解析的外部符号错误);如果找到了这两个函数,则根据具体情况(是Unicode字符集还是多字节字符集)选择mainCRTStartup或 wmainCRTStartup启动函数,再将启动函数嵌入到可执行文件中。
到目前为止,就生成了一个可执行文件。那接下来讲讲当运行了一个可执行文件,启动函数做了什么?

所有C/C++运行库启动函数所做的事情基本都是一样的,区别就在于它们要处理的是ANSI字符串,还是Unicode字符串;以及在初始化C运行库之后,它们调用的是哪一个入口点函数。
这些C运行时库函数,主要完成以下任务:
1.  获取进程命令行指针;
2.  获取进程环境变量指针;
3.  初始化C/C++运行时库的全局变量,如果你包含了头Stdlib.h,那么你就可以访问这些变量!初始化malloc函数的内存堆;
4.  为C++全局类,调用构造函数。

注意:malloc 函数,不要轻易使用?因为这个函数一般来说,最终会调用windows API函数,我们直接调用virtualAlloc的windowsAPI函数,效率会高!
让我们看下启动函数都初始化哪些全局变量,下面图示:
Windows核心编程之核心总结(第四章 进程(一))(2018.6.8)
Windows核心编程之核心总结(第四章 进程(一))(2018.6.8)
好了,我们知道了启动函数都做了些什么。当所有这些初始化操作完成后,C / C + +启动函数就调用应用程序的进入点函数。如果源文件写了一个_tWinMain,并且定义了_UNICODE(即项目属性设置为Unicode字符集),它将以下面的形式被调用 :

GetStartupInfo(&StartupInfo);
int nMainRetVal = wWinMain((HINSTANCE)&__ImageBase,
   NULL, pszCommandLineUnicode,
   (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? 
   StartupInfo.wShowWindow:SW_SHOWDEFAULT);

如果没有定义_UNICODE(即项目属性设置为多字节字符集),它将以下面的形式被调用 :

GetStartupInfo(&StartupInfo);
int nMainReLVal = WinMain((HINSTANCE)&__ImageBase,
   NULL, pszCommandLineANSI,
   (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? 
   Startupinfo.wShowWindow:SW_SHOWDEFAULT);

注意,上面的__ImageBase是一个链接器定义的伪变量,表明可执行文件被映射到进程地址空间的某个起始位置
如果源文件写了一个_tmain,并且定义了_UNICODE(即项目属性设置为Unicode字符集),它将以下面的形式被调用 :

int nMainRetVal = wmain(argc, wargv, wenviron); 

如果没有定义_UNICODE(即项目属性设置为多字节字符集),它将以下面的形式被调用 :

int nMainRetVal = main(argc, argv, environ);

童鞋们肯定好奇为什么在启动函数调用入口函数时,传入的参数不是全局变量argc、argv或 __wargv(这三个全局变量都有双下划线,排版问题所以没显示出来)等。那我们就进行源码剥析的测试:我先写了个CUI的程序,只有一个_tmain函数,然后调试,查看堆栈,双击我下方蓝色区域,看下执行到哪,会发现跳转到了入口函数的调用处,看来没错,参数确实是argc等。
Windows核心编程之核心总结(第四章 进程(一))(2018.6.8)
接着我们,看看这些argc, argv, environ到底在哪被赋值了,其实在本头文件上方的一个函数(_wgetmainargs)调用就被赋值了,但是由于我查看不到这个函数(_wgetmainargs)的定义,所以我猜测是函数里面就使用了我们之前所讲的双下划线的全局变量。总结一句话,微软的Windows真是太封闭了,源码没放出来真是难受呀。
Windows核心编程之核心总结(第四章 进程(一))(2018.6.8)

进程实例句柄(可执行文件实例句柄或者DLL文件实例句柄)

我们经过前面的学习都了解了,当运行一个程序时,会生成一个进程,然后进程有两个部分,其中一个部分就是进程地址空间,加载到进程地址空间的每一个可执行文件或者DLL文件都被赋予一个独一无二的实例句柄。这两种实例句柄分别来表示装入后的可执行文件,或者DLL,此时我们把这个可执行文件或者DLL叫做进程地址空间中的一个模块!进程实例句柄的本质,就是当前模块载入进程地址空间的起始地址。进程实例句柄的类型是HINSTANCE。学过Windows程序设计的童鞋都知道实例句柄的用处,在程序中很多地方,都被使用,尤其是在装入某一个资源的时候:

LoadIcon(
    HINSTANCE hInstance;
    PCTSTR pszIcon);

(1)由于经常在程序的其他地方需要使用到这个进程实例句柄,所以可以考虑将hInstance参数保存在一个全局变量,但俗话说得好,能不用全局变量就别用全局变量。为了迎合俗话,下面给出几个获取进程实例句柄的方法:

1.  (w)WinMain函数的第一个参数,可执行文件的实例句柄会在启动函数调用入口函数 (w)WinMain时传入。
2.  GetModuleHandle()函数返回指定文件名的实例句柄

下面是GetModuleHandle()函数签名:

HMODULE WINAPI GetModuleHandle(  
__in_opt  LPCTSTR lpModuleName//模块名称,其实就是可执行文件或者DLL文件的名称。
);

GetModuleHandle()函数获取的就是进程模块(可执行文件模块或DLL文件模块)在进程地址空间中的首地址!这个函数的使用注意事项:

1.  如果这个函数的参数是NULL的话,那么这个函数只返回当前可执行的模块地址!!
2.  在DLL中,调用GetModuleHandle,参数为NULL,那么这个函数返回的不是DLL模块的地址,而是当前可执行的模块地址!
3.  这个函数只检查本进程地址空间,不检查别的进程的地址空间。例如:如果一个ComDlg32.dll文件被载入了另一个B进程地址空间,那么 这个函数在A进程地址空间的代码中调用这个函数,这个函数不检查B的进程地址空间,所以在A进程地址空间没找到就返回NULL。

实际上,不管是(w)WinMain函数的第一个参数,还是GetModuleHandle函数获取的进程实例句柄,这个进程实例句柄都是指可执行文件或DLL文件模块载入进程地址空间的基地址。基地址默认是0x00400000,可以在项目->属性->链接器->高级处的基址、随机基址进行调整设置,先将随机基址设为否,再在基址填写“0x00100000”,这样每次运行应用程序,可执行文件或DLL文件都在0x00100000基址处开始。
下面对GetModuleHandle函数的使用进行测试:

#include<windows.h>
#include<tchar.h>
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, PTSTR pszCmdLine, int nCmdShow)
{
    //(1)测试点1:GetModuleHandle函数的使用,参数是模块文件名
    //windows程序中,一般都会有Kernel32.dll这个模块,那么现在我们就获得这个模块的句柄;
    HMODULE hModule1 = GetModuleHandle(L"Kernel32.dll");//Kernel32.dll动态链接库文件一般在程序中都会被嵌入到进程的地址空间去。
    HMODULE hModule2 = GetModuleHandle(NULL);
    HMODULE hModule3 = GetModuleHandle(L"Win32Project28.exe");
    //hInstance、hModule2和hModule3的值都是相等,因为GetModuleHandle(NULL)返回的是主调进程的可执行文件的实例句柄值。
    Return 0;
}

(2)如果要获取进程模块的文件名是什么?可以调用GetModuleFileHandle函数。
函数签名:

DWORD GetModuleFileName(
    HMODULE     hInstance,//进程句柄
    PTSTR       pszPath,//文件名
    DWORD       cchPath);//pszPath指向的内存的大小

在函数签名我们可以看到,HMODULE是什么类型的数据?在16位Windows中,HINSTANCE和HMODULE代表的是不同类型的数据。而现在的VS编译器有着这样的一条语句:typedef HINSTANCE HMODULE;说明其实现在的HINSTANCE和HMODULE都是同一个东西。
下面对GetModuleFileName函数的使用进行测试:

#include<windows.h>
#include<tchar.h>
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, PTSTR pszCmdLine, int nCmdShow)
{
    //(2)测试点2:GetModuleFileName函数的使用,
    //参数1是模块(加载到进程地址空间的每一个可执行文件或者DLL文件都属于一个模块)的实例句柄
    //参数2是模块文件的名称(绝对地址)
    //参数3是文件名的大小,可以设置为MAX_PATH->最大的路径长度
    TCHAR path1[MAX_PATH];
    TCHAR path2[MAX_PATH];
    GetModuleFileName(hModule1, path1, MAX_PATH);
    GetModuleFileName(hModule2, path2, MAX_PATH);
    Return 0;
}

(3)如果自己的代码位于一个DLL文件中,那么想知道这个DLL文件被装入进程控件后的模块地址怎么办?注意,下面两种方法的使用有两种情况,由于__ImageBase和GetModuleHandleEx函数都是返回当前模块(调用函数所在模块,例如下方的_tWinMain函数)的基地址,所以,如果下面两种方法在可执行文件的代码中使用,那么返回的就是可执行文件的基地址。而如果下面两种方法或函数在DLL文件的代码中使用,那么返回的就是DLL模块的基地址。举个例子:

#include<windows.h>
#include<tchar.h>
extern "C" HANDLE __ImageBase;
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE, PTSTR pszCmdLine, int nCmdShow)
{
    __ImageBase;
    HMODULE hModule4;
    GetModuleHandleEx(
        GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
        (PCTSTR)_tWinMain, &hModule4);//获取函数_tWinMain函数在哪个模块中运行。
    return 0;
}

测试结果图如下,__ImageBase和hModule4的值是相等的。
Windows核心编程之核心总结(第四章 进程(一))(2018.6.8)

猜你喜欢

转载自blog.51cto.com/12731497/2126553