您的位置:首页 > 编程语言

<<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是模块中的一个地址

贴一个相关的使用方法的代码:

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的代码写的很好,等以后看构建环境的时候再补上代码。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: