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

学习目标

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

C/C++程序编译过程

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

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],指针数组的取值情况如下图所示:

数组的各指针分别指向一个字符串。应当引起注意的是接收到的指针数组的各指针是从命令行的开始接收的,首先接收到的是命令,其后才是参数。

什么是进程

(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函数,效率会高!
让我们看下启动函数都初始化哪些全局变量,下面图示:


好了,我们知道了启动函数都做了些什么。当所有这些初始化操作完成后,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等。

接着我们,看看这些argc, argv, environ到底在哪被赋值了,其实在本头文件上方的一个函数(_wgetmainargs)调用就被赋值了,但是由于我查看不到这个函数(_wgetmainargs)的定义,所以我猜测是函数里面就使用了我们之前所讲的双下划线的全局变量。总结一句话,微软的Windows真是太封闭了,源码没放出来真是难受呀。

进程实例句柄(可执行文件实例句柄或者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的值是相等的。

原文地址:http://blog.51cto.com/12731497/2126553

时间: 2024-10-06 16:52:41

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

Python核心编程(第二版) 第二章习题答案 未完待续

2-2.程序输出.阅读下面的Python脚本.#!/usr/bin/env python1 + 2 * 4(a)你认为这段脚本是用来做什么的?(b)你认为这段脚本会输出什么?(c)输入以上代码,并保存为脚本,然后运行它,它所做的与你的预期一样吗?为什么一样/不一样?(d)这段代码单独执行和在交互解释器中执行有何不同?试一下,然后写出结果.(e)如何改进这个脚本,以便它能和你想象的一样工作?答:(a)这段脚本是用来计算表达式的值(b)脚本会输出9(c)保存为脚本,运行后没有输出.和自己预期不一样.

《Python核心编程》第二版第五章答案

5-1.整型.讲讲Python普通整型和长整型的区别. Python的标准整形类型是最通用的数字类型.在大多数32位机器上,标准整形类型的取值范围是-2**32-2**32 - 1. Python的长整型类型能表达的数值仅仅与你的机器支持的(虚拟)内存大小有关,换句话说,Python能轻松表达很大的整数. 长整型类型是标准整形类型的超集,当程序需要使用比标准整形更大的整型时,可以使用长整型类型,在整型值后面添加L,表示这个为长整型,这两种整形类型正在逐渐统一为 一种. 5-2.操作符.(a)写一

《Unix环境高级编程》读书笔记 第7章-进程环境

1. main函数 int main( int argc, char *argv[] ); argc是命令行参数的数目,包括程序名在内 argv是指向参数的各个指针所构成的数组,即指针数组 当内核执行C程序时(使用exec函数),在调用main前先调用一个特殊的启动例程.可执行程序文件将此启动例程指定为程序的起始地址——这是由连接器设置的,而连接器则是由C编译器调用.启动例程从内核取得命令行参数和环境变量值,然后按上述方式调用main函数做好安排. 2. 进程终止 有8种方式使进程终止,其中5种

Windows核心编程之核心总结(第一章 错误处理)(2018.5.26)

前沿 学习Windows核心编程是步入Windows编程殿堂的必经之路,2018年寒假重温了计算机操作系统知识,前阵子又过学习Windows程序设计方面的基础,正所谓打铁要乘热,所以我又入了Windows核心编程的坑啦,哈哈~ 学习目标 每一章的学习都要明确一个目标,就是你学完这一章之后你能做些什么?好的,我们一步步来学习第一章节错误处理.以下是这一章节的学习目标:1.了解Windows函数的错误机制2.了解GetLastError和SetLastError函数的使用3.了解FormatMess

Windows核心编程之核心总结(第三章 内核对象)(2018.6.2)

学习目标 第三章内核对象的概念较为抽象,理解起来着实不易,我不断上网找资料和看视频,才基本理解了内核对象的概念和特性,其实整本书给我的感觉就是完整代码太少了,没有多少实践的代码对内容的实现,而且书本给的源码例子,有太多我们不知道的知识,并且这些知识对本章主要内容来说是多余的,所以我们理解起来也非常困难.为了更好的学习这章,我补充了一些辅助性内容.这一章的学习目标:1.Windows会话和安全机制2.什么是内核对象?3.使用计数和安全描述符4.内核对象句柄表5.创建内核对象6.关闭内核对象7.跨进

Windows核心编程之核心总结(第四章 进程(二))(2018.6.17)

学习目标 上一节我们了解了进程.入口函数和进程实例句柄等内容,在进入进程的命令行学习前,有一个全局变量初始化问题需要测试一波.本节的学习目标如下:1.测试C/C++运行库启动函数初始化哪些全局变量2.进程的命令行3.进程的环境变量4.进程的当前驱动器和目录5.判断系统版本6.创建进程(CreateProcess函数详解) 测试启动函数初始化哪些全局变量 我们知道C/C++运行库的启动函数会做一些事后再调用我们的入口函数,而入口函数的参数都是在调用前就初始化好了的.那么我就产生了一个疑问,全局变量

Windows核心编程读书笔记-第四章进程

1.进程组成 一个内核对象,操作系统用它来管理进程. 一个地址空间,其中包含所有可执行文件或DLL模块的代码和数据.此外,它还包含动态内存分配,比如线程堆栈和堆的分配. 2.一个进程可以有多个线程,所有线程都在进程的地址空间中"同时"执行代码.每个进程至少要有一个线程来执行进程地址空间包含的代码. 3.用Microsoft Visual Studio来创建一个应用程序项目时,集开发环境会设置各种链接器开关,使链接器将子系统的正确类型嵌入最终生成的可执行文件.对于CUI程序,这个链接器开

Windows核心编程之核心总结(第四章 进程(三))(2018.6.21)

学习目标 本章节将学习以后经常用到的CreateProcess函数,听网上的人说有些面试官喜欢问这个函数的大概功能和参数作用哦,可见这个函数是十分重要滴,那我们来详细了解和测试这个函数的功能吧,有些不足的以后有实际经验再来修改和补充.说实话,我现在也只是一名大学生,到了实际开发也许才会用到这本书的内容,但我现在是作为兴趣来学这本书的,因为这本书给我的feel就是自己掌控Windows系统,这感觉太棒了.不管以后用不用的到,我觉得对我的帮助都很大.好了,闲话说到这吧,现在本章节的学习目标如下:1.

Windows核心编程之核心总结(第二章 字符和字符串处理)(2018.5.27)

学习目标 第二章是学习字符和字符串处理,为了更好理解这一章的内容,我自行添加了其他辅助性内容:存储模式(大端存储和小端存储).字符编码方案(一看就懂).以下是这一章的学习目标:1.大端存储和小端存储2.字符编码方案3.ANSI和Unicode字符.字符串,Windows自定义数据类型(为了兼容ANSI和Unicode)4.Windows的ANSI函数和Unicode函数5.C运行库的ANSI和Unicode函数6.C运行库的安全字符串函数7.C运行库的安全字符串函数(进阶版)8.字符串比较函数9