<<windows核心编程>>读书笔记---第4章 进程
2013-08-13 19:21
363 查看
前言:前几次读这章的时候,一直很懵懂,这次估计要整理好长时间了。。。
首先是进程的构成:
进程=一个内核对象+一个地址空间。
一个内核对象:操作系统用它来管理进程,其中也是系统保存进程统计信息的地方。
一个地址空间:其中包含所有的可执行文件或者dll模块的代码和数据,此外还包括动态内存分配,比如线程堆栈和堆的分配。
进程是有惰性的,他要想执行什么操作必须产生一个线程来执行地址空间的代码,如果没有线程,该进程就会自动销毁。
关于入口点函数和嵌入可执行文件的启动函数:
一个图片表格就可以说明一切:z
c/c++运行库的启动函数做的是如下的工作:
1. 获取指向新进程的完整命令行的一个指针。
2. 获取指向新进程的环境变量的一个指针。
3. 初始化c/c++运行库的全局变量。
4. 初始化c运行库内存分配函数(malloc和calloc)和其他底层I/O历程使用的堆
5. 调用所有全局和静态c++类对象的构造函数
6. 启动入口点函数。
7.当入口点函数返回之后,启动函数会调用c运行库的exit函数,向其传递 入口点函数的返回值,而exit将作如下操作:
7.1 调用on_exti函数调用所注册的任何一个函数
7.2 调用所有全局和静态c++类对象的析构函数
7.3 在DEBUG生成中,如果设置了_CRTDBG_LEAK_CHECK_DF标志,就通过调用 _CrtDumpMemoryLeaks函数来生成内存泄露报告。
7.4 调用操作系统的ExitProcess函数,向其传递入口点函数的返回值,这回导致操作系统杀死我们的进程,并设置 退出代码。
查看加载到的地址空间:
可执行文件加载的基地址,是由连接器/BASE:address连接器决定的,vs连接器使用的默认基地址是0x00400000,这也是可执行文件能加载到的最低地址。
如果需要查看一个可运行程序或者dll加载到的地址空间,可以调用GetModuleHandle()函数,该返回返回一个基地址。参数如果是NULL,则表示当前进程。
查看当前代码在哪个模块中执行:
1.使用__ImageBase伪变量,它指向我们正在运行的模块的基地址。注意extern "C" const IMAGE_DOS_HEADER __ImageBase;是必不可少的哦
2.调用GetModuleHandleEx();第一个参数:
如果是0,则当调用该函数时,模块的引用计数自动增加,调用者在使用完模块句柄后,必须调用一次FreeLibrary
如果是GET_MODULE_HANDLE_EX_FLAG_PIN,则模块一直映射在调用该函数的进程中,直到该进程结束,不管调用多少次FreeLibrary
如果是GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,则同GetModuleHandle相同,不增加引用计数
如果是GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,则lpModuleName是模块中的一个地址
贴一个相关的使用方法的代码:
进程的命令行:我们最好不要直接操作或者修改命令行缓冲区,建议将命令行缓冲区当做只读缓冲区对待,并且将命令行缓冲区复制到本地缓冲区来进行操作。
对命令行进行操作的函数一般有2个常用,GetCommandLine()和CommandLineToArgvW()。具体的使用方法详见代码:
ps: 第一项永远都是改程序的名称。
获得进程的环境变量:
1. 使用GetEnvironmentStrings();
2. CUI程序专用的 。在main函数的参数中有一个TCHRA env[],这是一个指针数组,里边存储了各个环境变量的指针。
判断一个环境变量是否存在:使用GetEnvironmentVariable()。第一个参数是预期的变量名称,第二个参数是 该变量的值,第三个参数是该变量值的缓冲区大小。
自然对应的有SetEnvironmentValue()函数来设置环境变量。如果该环境变量已经存在,则改写该值。如果不存在,则创建该值。
CreateProcess函数:
1.关于lpApplicationName和lpCommandLine参数
当lpApplicationName为NULL时,可以使用lpCommandLine来指定一个完整的命令行,供CreateProcess来创建进程。当CreateProcess解析lpApplicationName字符串的时候,它会检查字符串中的第一个标记,并假定此标记是我们想要运行的可执行文件的名称(如果没有后缀,默认加入.exe)。CreateProcess还会按照相应的顺序搜索可执行文件。,如果搜索到可执行文件,就创建一个进程,将可执行文件的代码和数据映射到新进程的地址空间,然后系统调用由链接器设置的运行时 启动函数,启动函数会检查进程的命令行,将可执行文件名之后的命令行传递给winmain的pszcommand参数。
当lpApplicationName不为NULL时,lpApplicationName参数必须有相应的扩展名,createprocess会假设该文件位于当前目录(除非指定了绝对地址),如果找不到就调用失败。反之,会将命令行完整的传递给该新进程。
2.lpProcessAttributes和lpThreadAttributes和bInheritHandles参数
lpProcessAttributes和lpThreadAttributes分别表明新创建的进程的进程内核对象和线程内核对象的安全属性。
bInheritHandles表明了新创建的进程能否进程该进程的内核对象。
3. dwCreationFlags参数有很多中标志,具体可以网络查询。略。
4. lpEnvironment指向一块内存,其中包含了新进程要使用的环境字符串。
5. lpStartupInfo参数指向一个STARTUPINFO结构或者STARTUPINFOEX结构。使用的时候需要初始化,而第一个cb变量是必须的。
进程的终止方法:
1. 主线程的入口点函数返回(强烈推荐这种方式)
2. 进程中一个线程调用ExitProcess函数(要避免使用这种方法)
3. 另一个进程中的线程调用TerminateProcess函数(要避免使用这种方法)
4. 进程中的线程都“自然死亡”(这种情况几乎从来不会发生)。
主线程的入口点函数返回 来结束进程,可以保证以下操作:
1. 该线程创建的任何c++对象都将由这些对象的析构函数正确销毁。
2. 操作系统将正确释放线程栈使用的内存
3. 系统将进程的退出代码(在进程内核对象中维护)设为入口点函数的返回值。
4. 系统递减进程内核对象的使用计数。
ExitProcess函数:
如果在main函数中调用ExitProcess函数,会导致资源无法释放的问题。因为c++运行库启动函数在入口点函数返回之后,会清理该进程使用的所有资源包括对象的析构,运行库函数还会调用ExitProcess来结束该进程。而直接在入口点函数调用ExitProcess函数,会导致直接退出该线程,没有了后续的操作。例子来说话:
看到了吧,完全没有调用析构函数啊。。。这么可怕的东西。
平时喜欢用exit(0)来结束程序,顾也做了测试:
看到了吧,exit(0)的话局部变量是没有析构的。
TerminateProcess函数:
与ExitProcess的不同是,任何进程都可以调用TerminateProcess来终止另一个进程或者自己的进程。而ExitProcess值可以终止自己的进程。
当进程终止时,系统会依次执行以下操作:
1. 终止进程中遗留的任何线程
2. 释放进程分配的所有用户对象和GDI对象。
3. 进程的退出代码从STILL_ALIVE变化为ExitProcess或者TerminateProcess函数的参数。
4. 进程内核对象的状态变为已触发状态。
5. 进程的内核对象的使用计数减1.
进程查询以后补上。Jeffrey的代码写的很好,等以后看构建环境的时候再补上代码。
首先是进程的构成:
进程=一个内核对象+一个地址空间。
一个内核对象:操作系统用它来管理进程,其中也是系统保存进程统计信息的地方。
一个地址空间:其中包含所有的可执行文件或者dll模块的代码和数据,此外还包括动态内存分配,比如线程堆栈和堆的分配。
进程是有惰性的,他要想执行什么操作必须产生一个线程来执行地址空间的代码,如果没有线程,该进程就会自动销毁。
关于入口点函数和嵌入可执行文件的启动函数:
一个图片表格就可以说明一切:z
c/c++运行库的启动函数做的是如下的工作:
1. 获取指向新进程的完整命令行的一个指针。
2. 获取指向新进程的环境变量的一个指针。
3. 初始化c/c++运行库的全局变量。
4. 初始化c运行库内存分配函数(malloc和calloc)和其他底层I/O历程使用的堆
5. 调用所有全局和静态c++类对象的构造函数
6. 启动入口点函数。
7.当入口点函数返回之后,启动函数会调用c运行库的exit函数,向其传递 入口点函数的返回值,而exit将作如下操作:
7.1 调用on_exti函数调用所注册的任何一个函数
7.2 调用所有全局和静态c++类对象的析构函数
7.3 在DEBUG生成中,如果设置了_CRTDBG_LEAK_CHECK_DF标志,就通过调用 _CrtDumpMemoryLeaks函数来生成内存泄露报告。
7.4 调用操作系统的ExitProcess函数,向其传递入口点函数的返回值,这回导致操作系统杀死我们的进程,并设置 退出代码。
查看加载到的地址空间:
可执行文件加载的基地址,是由连接器/BASE:address连接器决定的,vs连接器使用的默认基地址是0x00400000,这也是可执行文件能加载到的最低地址。
如果需要查看一个可运行程序或者dll加载到的地址空间,可以调用GetModuleHandle()函数,该返回返回一个基地址。参数如果是NULL,则表示当前进程。
查看当前代码在哪个模块中执行:
1.使用__ImageBase伪变量,它指向我们正在运行的模块的基地址。注意extern "C" const IMAGE_DOS_HEADER __ImageBase;是必不可少的哦
2.调用GetModuleHandleEx();第一个参数:
如果是0,则当调用该函数时,模块的引用计数自动增加,调用者在使用完模块句柄后,必须调用一次FreeLibrary
如果是GET_MODULE_HANDLE_EX_FLAG_PIN,则模块一直映射在调用该函数的进程中,直到该进程结束,不管调用多少次FreeLibrary
如果是GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,则同GetModuleHandle相同,不增加引用计数
如果是GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,则lpModuleName是模块中的一个地址
贴一个相关的使用方法的代码:
extern "C" const IMAGE_DOS_HEADER __ImageBase; #pragma comment(lib,"DllTest.lib") extern "C" _declspec(dllimport) void PrintDLL(); void test() { HMODULE hmodule=GetModuleHandle(NULL); //传入NULL,获得当前进程的内存基地址 _tprintf(_T("GetModuleHandle(NULL):0x%x\r\n"),hmodule); _tprintf(_T("_ImageBase:0x%x\r\n"),(HINSTANCE)&__ImageBase); //使用伪变量 hmodule=NULL; GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,(PTSTR)test,&hmodule); //调用GetModuleHandleEx函数 _tprintf(_T("GetModuleHandleEx():0x%x\r\n"),hmodule); PrintDLL(); //这是DLL的导入函数,dll里调用了GetModuleHandle(NULL),不过得到的仍是当前进程的地址空间 hmodule=NULL; hmodule=GetModuleHandle(_T("Dlltest.dll")); //获取DLL的内存地址 _tprintf(_T("GetModuleHandle(DLL):0x%x\r\n"),hmodule); } int main() { test(); return 0; }
进程的命令行:我们最好不要直接操作或者修改命令行缓冲区,建议将命令行缓冲区当做只读缓冲区对待,并且将命令行缓冲区复制到本地缓冲区来进行操作。
对命令行进行操作的函数一般有2个常用,GetCommandLine()和CommandLineToArgvW()。具体的使用方法详见代码:
int main() { //已经在属性设置中填入了“woo jing test”命令行参数 int commandline_num=4; LPWSTR ptr=GetCommandLine(); LPWSTR *ppArgv=CommandLineToArgvW(GetCommandLine(),&commandline_num); for (int i=0;i<commandline_num;i++) { printf("%d: %ws\n", i, ppArgv[i]); } HeapFree(GetProcessHeap(),0,ppArgv); return 0; }
ps: 第一项永远都是改程序的名称。
获得进程的环境变量:
1. 使用GetEnvironmentStrings();
void DumpEnvStrings() { PTSTR pEnvBlock=GetEnvironmentStrings(); //获取环境变量字符串 TCHAR szName[MAX_PATH]; //来存储环境变量=号前边的部分 TCHAR szValue[MAX_PATH]; //来存储环境变量=号右边的部分 PTSTR pszCurrent=pEnvBlock; //将环境变量存入本地缓冲区 HRESULT hr=S_OK; PCTSTR pszPos=NULL; int current=0; while (pszCurrent!=NULL) { if ((*pszCurrent) != TEXT('=')) { pszPos=_tcschr(pszCurrent,TEXT('=')); //只留下=号右边的部分 pszPos++; //去掉=号 size_t cbNameLength=(size_t)pszPos-(size_t)pszCurrent-sizeof(TCHAR); //=号左边的名称有多长 hr=StringCbCopyN(szName,MAX_PATH,pszCurrent,cbNameLength); //=号左边的部分复制到szname if (FAILED(hr)) { break; } hr=StringCchCopyN(szValue,MAX_PATH,pszPos,_tcslen(pszPos)+1); //=号右边的部分复制到szvalue if (SUCCEEDED(hr)) { _tprintf(TEXT("[%u] %s=%s\r\n"),current,szName,szValue); //输出 XXX=XXX current++; } else if (hr==STRSAFE_E_INSUFFICIENT_BUFFER) { _tprintf(TEXT("[%u] %s=%s...\r\n"),current,szName,szValue); } else { _tprintf(TEXT("[%u] %s=???\r\n"),current,szName); } break; } while (*pszCurrent!=TEXT('\0')) { pszCurrent++; } pszCurrent++; if (*pszCurrent==TEXT('\0')) { break; } } FreeEnvironmentStrings(pEnvBlock); }
2. CUI程序专用的 。在main函数的参数中有一个TCHRA env[],这是一个指针数组,里边存储了各个环境变量的指针。
void DumpEnvStrings(PTSTR pEnvBlock[]) { int current=0; PTSTR *pElement=(PTSTR*)pEnvBlock; PTSTR pCurrent=NULL; while(pElement!=NULL) { pCurrent=(PTSTR)(*pElement); if (pCurrent==NULL) { pElement=NULL; } else { _tprintf(TEXT("[%u] %s\r\n"),current,pCurrent); current++; pElement++; } } }
判断一个环境变量是否存在:使用GetEnvironmentVariable()。第一个参数是预期的变量名称,第二个参数是 该变量的值,第三个参数是该变量值的缓冲区大小。
void CheckEnvironmentValue() { LPCWSTR str=_T("PATH"); PTSTR szValue=NULL; DWORD buffersize=GetEnvironmentVariable(str,szValue,0); //先调用一次,因为szvalue没有分配空间,先调用一次返回缓冲区的大小。 if (buffersize==0) { wprintf(_T("can not find this environmentvalue.\r\n")); } else { szValue=(PTSTR)malloc(sizeof(TCHAR)*buffersize); GetEnvironmentVariable(str,szValue,sizeof(TCHAR)*buffersize); //第二次调用,已经分配好了缓冲区,直接复制进来即可。 _tprintf(_T("the environmentvalue is :%s"),szValue); } }
自然对应的有SetEnvironmentValue()函数来设置环境变量。如果该环境变量已经存在,则改写该值。如果不存在,则创建该值。
CreateProcess函数:
BOOL WINAPI CreateProcess( _In_opt_ LPCTSTR lpApplicationName, _Inout_opt_ LPTSTR lpCommandLine, _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes, _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ BOOL bInheritHandles, _In_ DWORD dwCreationFlags, _In_opt_ LPVOID lpEnvironment, _In_opt_ LPCTSTR lpCurrentDirectory, _In_ LPSTARTUPINFO lpStartupInfo, _Out_ LPPROCESS_INFORMATION lpProcessInformation );
1.关于lpApplicationName和lpCommandLine参数
当lpApplicationName为NULL时,可以使用lpCommandLine来指定一个完整的命令行,供CreateProcess来创建进程。当CreateProcess解析lpApplicationName字符串的时候,它会检查字符串中的第一个标记,并假定此标记是我们想要运行的可执行文件的名称(如果没有后缀,默认加入.exe)。CreateProcess还会按照相应的顺序搜索可执行文件。,如果搜索到可执行文件,就创建一个进程,将可执行文件的代码和数据映射到新进程的地址空间,然后系统调用由链接器设置的运行时 启动函数,启动函数会检查进程的命令行,将可执行文件名之后的命令行传递给winmain的pszcommand参数。
当lpApplicationName不为NULL时,lpApplicationName参数必须有相应的扩展名,createprocess会假设该文件位于当前目录(除非指定了绝对地址),如果找不到就调用失败。反之,会将命令行完整的传递给该新进程。
2.lpProcessAttributes和lpThreadAttributes和bInheritHandles参数
lpProcessAttributes和lpThreadAttributes分别表明新创建的进程的进程内核对象和线程内核对象的安全属性。
bInheritHandles表明了新创建的进程能否进程该进程的内核对象。
3. dwCreationFlags参数有很多中标志,具体可以网络查询。略。
4. lpEnvironment指向一块内存,其中包含了新进程要使用的环境字符串。
5. lpStartupInfo参数指向一个STARTUPINFO结构或者STARTUPINFOEX结构。使用的时候需要初始化,而第一个cb变量是必须的。
进程的终止方法:
1. 主线程的入口点函数返回(强烈推荐这种方式)
2. 进程中一个线程调用ExitProcess函数(要避免使用这种方法)
3. 另一个进程中的线程调用TerminateProcess函数(要避免使用这种方法)
4. 进程中的线程都“自然死亡”(这种情况几乎从来不会发生)。
主线程的入口点函数返回 来结束进程,可以保证以下操作:
1. 该线程创建的任何c++对象都将由这些对象的析构函数正确销毁。
2. 操作系统将正确释放线程栈使用的内存
3. 系统将进程的退出代码(在进程内核对象中维护)设为入口点函数的返回值。
4. 系统递减进程内核对象的使用计数。
ExitProcess函数:
如果在main函数中调用ExitProcess函数,会导致资源无法释放的问题。因为c++运行库启动函数在入口点函数返回之后,会清理该进程使用的所有资源包括对象的析构,运行库函数还会调用ExitProcess来结束该进程。而直接在入口点函数调用ExitProcess函数,会导致直接退出该线程,没有了后续的操作。例子来说话:
class A { public: A() {cout<<"construct"<<endl;} ~A() {cout<<"disconstruct"<<endl;} }; A g_a; int main() { A local_a; ExitProcess(0); return 0; }
看到了吧,完全没有调用析构函数啊。。。这么可怕的东西。
平时喜欢用exit(0)来结束程序,顾也做了测试:
class A { public: A() {cout<<"construct"<<endl;} ~A() {cout<<"disconstruct"<<endl;} }; A g_a; int main() { A local_a; exit(0); return 0; }
class A { public: A(int m) {kiss=m;cout<<"construct"<<kiss<<endl;} ~A() {cout<<"disconstruct:"<<kiss<<endl;} int kiss; }; A g_a(3); int main() { A local_a(2); exit(0); return 0; //无视我 其实根本调用不到 }
看到了吧,exit(0)的话局部变量是没有析构的。
TerminateProcess函数:
与ExitProcess的不同是,任何进程都可以调用TerminateProcess来终止另一个进程或者自己的进程。而ExitProcess值可以终止自己的进程。
当进程终止时,系统会依次执行以下操作:
1. 终止进程中遗留的任何线程
2. 释放进程分配的所有用户对象和GDI对象。
3. 进程的退出代码从STILL_ALIVE变化为ExitProcess或者TerminateProcess函数的参数。
4. 进程内核对象的状态变为已触发状态。
5. 进程的内核对象的使用计数减1.
进程查询以后补上。Jeffrey的代码写的很好,等以后看构建环境的时候再补上代码。
相关文章推荐
- <<windows核心编程>>读书笔记---第9章 内核对象进行线程同步
- <<windows核心编程>>读书笔记---第6章 线程基础
- <<windows核心编程>>读书笔记---第8章 用户模式下的线程同步
- <<Linux内核的设计与实现>>读书笔记(三)-Linux的进程
- <<C++Primer PLus 第五版>>读书笔记4
- <<Hive编程指南>>读书笔记
- <<C++代码设计与重用>>读书笔记(一) Nice 类
- <<Effective C++>>读书笔记3: 资源管理
- <<More Effective C++>>读书笔记3: 异常
- <<一线架构师实践指南>>读书笔记之二----PA阶段
- Windows核心编程<读书笔记四> 进程的概念
- <<深度探索c++对象模型>>第一章读书笔记
- <<读书笔记>>系列--VB2005-菜根谭
- <<C++Primer PLus 第五版>>读书笔记3
- <<沧浪之水>> 读书笔记
- <<More Effective C++>>读书笔记6: 杂项
- <<读书笔记>>系列--Once And Only Once
- <<沿途的向阳花>>读书笔记
- <<iText in Action 2nd>>4.2节(Changing the properties of a cell)读书笔记
- <<C++的设计与演化>>读书笔记(四)