第二章--Win32程序运行原理 (部分概念及代码讲解)

时间:2023-03-09 07:52:15
第二章--Win32程序运行原理        (部分概念及代码讲解)

学习《Windows程序设计》记录

概念贴士:

1.  每个进程都有赋予它自己的私有地址空间。当进程内的线程运行时,该线程仅仅能够访问属于它的进程的内存,而属于其他进程的内存被屏蔽了起来,不能被该线程访问。

   PS:进程A在其地址空间的0x12345678地址处能够有一个数据结构,而进程B能够在其地址空间的0x12345678处存储一个完全不同的数据。彼此不能访问。

2.  在大多数系统中,Windows将地址空间的一半(4GB的前一半,0x00000000-0x7FFFFFFF)留给进程作为私有存储,自己使用另一半(4GB的后一半,0x80000000-0xFFFFFFFFF)来存储操作系统内部使用的数据。

3.  各进程的地址空间被分成了用户空间和系统空间两部分。用户空间部分是进程私有地址空间,一个进程不能一任何方式读、写其他进程此部分空间中的数据。系统空间部分放置操作系统的代码,包括内核代码、设备驱动代码、设备I/O缓冲区等。系统空间部分在所有的进程中是共享的。在部分系统中,这些数据结构是被保护的。试图访问时,会遇到访问异常。

4.  处理器定义了多个特权级别。如80386处理器共定义了4种(0-3)特权级别,或者称为环。其中0级是*(特权级),3级是最低级(用户级)。

5.  为了阻止应用应用程序访问或者修改关键的系统数据(即2GB系统空间内的数据),Windows使用了两种访问模式:内核模式和用户模式,它们分别使用了处理器中的0和3这两个特权级别。用户程序的代码在用户模式下运行,系统程序(如系统服务程序和硬件驱动)的代码在内核模式下运行。

6.  内核对象是系统提供的用户模式下代码与内核模式下代码进行交互的基本接口。使用内核交互对象是应用程序和系统内核进行交互的重要方式之一。

7.  引入内核对象后,系统可以较为方便的完成以下4个任务:

    1)为系统资源提供可识别的名字;

    2)在进程之间共享资源和数据;

    3)保护资源不会被未经许可的代码访问;

    4)跟踪对象的引用情况,这使得系统知道什么时候一个对象不再被使用了,以便释放它占用的空间。

8.  内核对象的数据结构仅能够从内核模式访问,所以直接在内存中定位这些数据结构对应用程序来说是不可能的。应用程序必须使用API函数来访问内核对象。

9.  调用函数创建内核对象时,函数会返回标识此内核对象的句柄。句柄是进程相关的,仅对创建该内核对象的进程有效。当然,多个进程共享一个内核对象也是可能的,调用DuplicateHandle函数复制一个进程句柄传给其他进程即可。

10.  内核对象中最简单最常用的属性---使用计数。使用计数属性指明进程对特定内核对象的应用次数,当系统发现引用次数为0时,它会自动关闭资源。(创建时初始化为1,每次打开这个内核对象时,计数加1,关闭则减1。当减到0时,说明进程对这个内核对象的所有引用都已经关闭了,系统应该释放此内核对象资源了。)

11.  进程(Process)是一个正在运行的程序,它拥有自己的虚拟地址空间,拥有自己的代码、数据和其他系统资源,如进程创建的文件、管道、同步对象等。一个进程也包含一个或者多个运行在此进程内的线程(Thread)。

12.  程序与进程在表面很相似。但是,程序是一连串静态的指令,而进程是一个容器,它包含了一系列运行在这个程序实例上下问中的线程使用的资源。

13.  线程是进程内执行代码的独立实体。操作系统创建了进程后,会创建一个线程执行进程中的代码。通常称这个线程为主线程。主线程在运行过程中可能会创建其他线程(称为辅助线程)。

14.  Win进程两个组成部分:

    1)进程内核对象。操作系统使用此内核对象来管理该进程。这个内核对象也是操作系统存放进程统计信息的地方。

    2)私有的虚拟地址空间。此地址空间包含了所有可执行的或者是DLL模块的代码和数据,它也是程序动态申请内存的地方,比如说线程堆栈和进程堆。

15.  应用程序必须有一个入口函数,它在程序开始运行的时候被调用。如果创建的是控制台应用程序,此入口函数就是main。

    PS:int main(int argc,char *argv[]);

16.  事实上,操作系统并不是真的调用main函数,而是去调用C/C++运行期启动函数,此函数会初始化C/C++运行期库。因此,在程序中可以调用malloc和free之类的函数。

17.  在控制台程序中,C/C++运行期启动函数会调用程序入口函数main。如果没有,将会返回“unresolved external symbol”错误。

18.  Win32程序的启动过程就是进程的创建过程,操作系统通过调用CreateProcess函数(代码解释中将会有对该函数的具体解释)来创建新的进程的。当一个线程调用CreateProcess函数的时候,系统会创建一个进程内核对象,其使用计数被初始化为1.此进程内核对象不是进程本身,仅仅是一个系统用来管理这个进程的小的数据结构。系统然后会为新的进程创建一个虚拟地址空间,加载应用程序运行时所需要的代码和数据。系统接着会为新进程创建一个主线程,这个主线程通过执行C/C++运行期启动代码开始运行,C/C++运行期启动代码又会调用main函数。如果系统能够成功创建新的进程和进程的主线程,CreateProcess函数会返回TRUE,否则会返回FALSE。

19.  一般将创建进程称为父进程,被创建的进程称为子进程。系统在创建新的进程时会为新进程指定一个STARTUPINFO类型的变量,这个结构包含了父进程传递给子进程的一些显示信息。(对图形界面应用程序来说,这些信息会影响新的进程中主线程的主窗口的显示等。)

代码解释:

1.CreateProcess(create process)

  PS:一个完整的创建进程的程序,打开了Windows自带的命令行程序cmd.exe。(不过我一般是Win+R,直接cmd进入。)

 #include "stdafx.h"
#include <windows.h>
#include <stdio.h> int main(int argc, char* argv[])
{
char szCommandLine[] = "cmd";
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi; si.dwFlags = STARTF_USESHOWWINDOW; // 指定wShowWindow成员有效
si.wShowWindow = TRUE; // 此成员设为TRUE的话则显示新建进程的主窗口,
// 为FALSE的话则不显示
BOOL bRet = ::CreateProcess (
NULL, // 不在此指定可执行文件的文件名
szCommandLine, // 命令行参数
NULL, // 默认进程安全性
NULL, // 默认线程安全性
FALSE, // 指定当前进程内的句柄不可以被子进程继承
CREATE_NEW_CONSOLE, // 为新进程创建一个新的控制台窗口
NULL, // 使用本进程的环境变量
NULL, // 使用本进程的驱动器和目录
&si,
&pi); if(bRet)
{
// 既然我们不使用两个句柄,最好是立刻将它们关闭
::CloseHandle (pi.hThread);
::CloseHandle (pi.hProcess); printf(" 新进程的进程ID号:%d \n", pi.dwProcessId);
printf(" 新进程的主线程ID号:%d \n", pi.dwThreadId);
}
return ;
}

2.ProcessList(process list)

  PS:使用ToolHelp函数中的CreateToolhelp32Snapshop函数给当前系统内执行的进程拍快照(Snapshot),获取进程列表。然后利用Process32First函数和Process32Next函数遍历快照中的记录的列表。

 #include "stdafx.h"
#include <windows.h>
#include <tlhelp32.h> // 声明快照函数的头文件 int main(int argc, char* argv[])
{
PROCESSENTRY32 pe32;
// 在使用这个结构之前,先设置它的大小
pe32.dwSize = sizeof(pe32); // 给系统内的所有进程拍一个快照
HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, );
if(hProcessSnap == INVALID_HANDLE_VALUE)
{
printf(" CreateToolhelp32Snapshot调用失败! \n");
return -;
} // 遍历进程快照,轮流显示每个进程的信息
BOOL bMore = ::Process32First(hProcessSnap, &pe32);
while(bMore)
{
printf(" 进程名称:%s \n", pe32.szExeFile);
printf(" 进程ID号:%u \n\n", pe32.th32ProcessID); bMore = ::Process32Next(hProcessSnap, &pe32);
} // 不要忘记清除掉snapshot对象
::CloseHandle(hProcessSnap);
return ;
}

3.TerminateProcess(terminate process)

  PS:该程序通过输入进程独有的ID号,来结束该进程,兵返回操作结果。(你们可以从任务管理器找一个进程ID试试,建议拿QQ什么的试。千万别找Windows什么的)

 #include "stdafx.h"
#include <windows.h> BOOL TerminateProcessFromId(DWORD dwId)
{
BOOL bRet = FALSE;
// 打开目标进程,取得进程句柄
HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwId);
if(hProcess != NULL)
{
// 终止进程
bRet = ::TerminateProcess(hProcess, );
}
CloseHandle(hProcess);
return bRet;
} int main(int argc, char* argv[])
{
DWORD dwId;
printf(" 请输入您要终止的进程的ID号: \n");
scanf("%u", &dwId);
if(TerminateProcessFromId(dwId))
{
printf(" 终止进程成功! \n");
}
else
{
printf(" 终止进程失败! \n");
} return ;
}

4.Testor

  PS:这个是为了这章最后一个程序--内存修改器服务的一个测试程序。后面会通过内存修改器修改g_nNum和i的值。

 #include "stdafx.h"
#include <stdio.h>
// 全局变量测试
int g_nNum;
int main(int argc, char* argv[])
{
int i = ; // 局部变量测试
g_nNum = ; while()
{
// 输出个变量的值和地址
printf(" i = %d, addr = %08lX; g_nNum = %d, addr = %08lX \n",
++i, &i, --g_nNum, &g_nNum);
getchar();
} return ;
}

5.MemRepair(memory repair)

  PS:这章最后一个程序--内存修改器。具体说明程序备注有写。(大家可以试试,游戏挂了不负责。另外,在第七章后将会有一个GUI版本的。)

 #include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include <iostream.h> BOOL FindFirst(DWORD dwValue); // 在目标进程空间进行第一次查找
BOOL FindNext(DWORD dwValue); // 在目标进程地址空间进行第2、3、4……次查找 DWORD g_arList[]; // 地址列表
int g_nListCnt; // 有效地址的个数
HANDLE g_hProcess; // 目标进程句柄 ////////////////////// BOOL WriteMemory(DWORD dwAddr, DWORD dwValue);
void ShowList(); int main(int argc, char* argv[])
{
// 启动02testor进程
char szFileName[] = "..\\02testor\\debug\\02testor.exe";
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
::CreateProcess(NULL, szFileName, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
// 关闭线程句柄,既然我们不使用它
::CloseHandle(pi.hThread);
g_hProcess = pi.hProcess; // 输入要修改的值
int iVal;
printf(" Input val = ");
scanf("%d", &iVal); // 进行第一次查找
FindFirst(iVal); // 打印出搜索的结果
ShowList(); while(g_nListCnt > )
{
printf(" Input val = ");
scanf("%d", &iVal); // 进行下次搜索
FindNext(iVal); // 显示搜索结果
ShowList();
} // 取得新值
printf(" New value = ");
scanf("%d", &iVal); // 写入新值
if(WriteMemory(g_arList[], iVal))
printf(" Write data success \n"); ::CloseHandle(g_hProcess);
return ;
} BOOL CompareAPage(DWORD dwBaseAddr, DWORD dwValue)
{
// 读取1页内存
BYTE arBytes[];
if(!::ReadProcessMemory(g_hProcess, (LPVOID)dwBaseAddr, arBytes, , NULL))
return FALSE; // 此页不可读 // 在这1页内存中查找
DWORD* pdw;
for(int i=; i<(int)*-; i++)
{
pdw = (DWORD*)&arBytes[i];
if(pdw[] == dwValue) // 等于要查找的值?
{
if(g_nListCnt >= )
return FALSE;
// 添加到全局变量中
g_arList[g_nListCnt++] = dwBaseAddr + i;
}
} return TRUE;
} BOOL FindFirst(DWORD dwValue)
{
const DWORD dwOneGB = **; // 1GB
const DWORD dwOnePage = *; // 4KB if(g_hProcess == NULL)
return FALSE; // 查看操作系统类型,以决定开始地址
DWORD dwBase;
OSVERSIONINFO vi = { sizeof(vi) };
::GetVersionEx(&vi);
if (vi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS)
dwBase = **; // Windows 98系列,4MB
else
dwBase = *; // Windows NT系列,64KB // 在开始地址到2GB的地址空间进行查找
for(; dwBase < *dwOneGB; dwBase += dwOnePage)
{
// 比较1页大小的内存
CompareAPage(dwBase, dwValue);
} return TRUE;
} BOOL FindNext(DWORD dwValue)
{
// 保存m_arList数组中有效地址的个数,初始化新的m_nListCnt值
int nOrgCnt = g_nListCnt;
g_nListCnt = ; // 在m_arList数组记录的地址处查找
BOOL bRet = FALSE; // 假设失败
DWORD dwReadValue;
for(int i=; i<nOrgCnt; i++)
{
if(::ReadProcessMemory(g_hProcess, (LPVOID)g_arList[i], &dwReadValue, sizeof(DWORD), NULL))
{
if(dwReadValue == dwValue)
{
g_arList[g_nListCnt++] = g_arList[i];
bRet = TRUE;
}
}
} return bRet;
} // 打印出搜索到的地址
void ShowList()
{
for(int i=; i< g_nListCnt; i++)
{
printf("%08lX \n", g_arList[i]);
}
} BOOL WriteMemory(DWORD dwAddr, DWORD dwValue)
{
return ::WriteProcessMemory(g_hProcess, (LPVOID)dwAddr, &dwValue, sizeof(DWORD), NULL);
}

总结:重要的概念、知识点、代码讲解都被标识出来了。通过这章可以对Win32程序运行等原理有着清晰的了解。真正地懂得了程序运行的完整过程、了解内存的实际情况。改变了以前对Win32程序认识的不清晰。