首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
269 阅读
2
从零开始配置 vim(15)——状态栏配置
159 阅读
3
nvim番外之将配置的插件管理器更新为lazy
145 阅读
4
2018总结与2019规划
137 阅读
5
PDF标准详解(五)——图形状态
116 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
emacs
读书笔记
elisp
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
392
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
90
篇与
的结果
2016-09-06
windows 多任务与进程
多任务,进程与线程的简单说明多任务的本质就是并行计算,它能够利用至少2处理器相互协调,同时计算同一个任务的不同部分,从而提高求解速度,或者求解单机无法求解的大规模问题。以前的分布式计算正是利用这点,将大规模问题分解为几个互不不相关的问题,将这些计算问题交给局域网中的其他机器计算完成,然后再汇总到某台机器上,显示结果,这样就充分利用局域网中的计算机资源。相对的,处理完一步接着再处理另外一步,将这样的传统计算模式称为串行计算。在提高处理器的相关性能主要有两种方式,一种是提高单个处理器处理数据的速度,这个主要表现在CPU主频的调高上,而当前硬件总有一个上限,以后再很难突破,所以现在的CPU主要采用的是调高CPU的核数,这样CPU的每个处理器都处理一定的数据,总体上也能带来性能的提升。在某些单核CPU上Windows虽然也提供了多任务,但是这个多任务是分时多任务,也就是每个任务只在CPU中执行一个固定的时间片,然后再切换到另一个任务,由于每个任务的时间片很短,所以给人的感觉是在同一时间运行了多个任务。单核CPU由于需要来回的在对应的任务之间切换,需要事先保存当前任务的运行环境,然后通过轮循算法找到下一个运行的任务,再将CPU中寄存器环境改成新任务的环境,新任务运行到达一定时间,又需要重复上述的步骤,所以在单核CPU上使用多任务并不能带来性能的提升,反而会由在任务之间来回切换,浪费宝贵的资源,多任务真正使用场合是多核的CPU上。windows上多任务的载体是进程和线程,在windows中进程是不执行代码的,它只是一个载体,负责从操作系统内核中分配资源,比如每个进程都有4GB的独立的虚拟地址空间,有各自的内核对象句柄等等。线程是资源分配的最小单元,真正在使用这些资源的是线程。每个程序都至少有一个主线程。线程是可以被执行的最小的调度单位。进程的亲缘性进程或者线程只在某些CPU上被执行,而不是由系统随机分配到任何可用的CPU上,这个就是进程的亲缘性。例如某个CPU有8个处理器,可以通过进程的亲缘性设置让该进程的线程只在某两个处理器上运行,这样就不会像之前那样在8个CPU中的任意几个上运行。在windows上一般更倾向于优先考虑将线程安排在之前执行它的那个处理器上运行,因为之前的处理器的高速缓存中可能存有这个线程之前的执行环境,这样就提高的高速缓存的命中率,减少了从内存中取数据的次数能从一定程度上提高性能。需要注意的是,在拥有三级高速缓存的CPU上,这么做意义就不是很大了,因为三级缓存一般作为共享缓存,由所有处理器共享,如果之前在2号处理器上执行某个线程,在三级缓存上留下了它的运行时的数据,那么由于三级缓存是由所有处理器所共享的,这个时候即使将这个线程又分配到5号处理器上,也能访问这个公共存储区,所以再设置线程在固定的处理器上运行就显得有些鸡肋,这也是拥有三级缓存的CPU比二级缓存的CPU占优势的地方。但是如果CPU只具有二级缓存,通过设置亲缘性可以发挥高速缓存的优势,提高效率。亲缘性可以通过函数SetProcessAffinityMask设置。该函数的原型如下:BOOL WINAPI SetProcessAffinityMask( __in HANDLE hProcess, __in DWORD_PTR dwProcessAffinityMask );第一个参数是设置的进程的句柄,第二个参数是DWORD类型的指针,是一个32位的整数,每一位代表一个处理器的编号,当希望设置进程中的线程在此处理器上运行的话,将该位设置为1,否则为0。为了设置亲缘性,首先应该了解机器上CPU的情况,根据实际情况来妥善安排,获取CPU的详细信息可以通过函数GetLogicalProcessInformation来完成。该函数的原型如下:BOOL WINAPI GetLogicalProcessorInformation( __out PSYSTEM_LOGICAL_PROCESSOR_INFORMATION Buffer, __in_out PDWORD ReturnLength );第一个参数是一个结构体的指针,第二个参数是需要缓冲区的大小,也就是说这个函数我们可以采用两次调用的方式来合理安排缓冲区的大小。结构体SYSTEM_LOGICAL_PROCESSOR_INFORMATION的定义如下: typedef struct _SYSTEM_LOGICAL_PROCESSOR_INFORMATION { ULONG_PTR ProcessorMask; LOGICAL_PROCESSOR_RELATIONSHIP Relationship; union { struct { BYTE Flags; } ProcessorCore; struct { DWORD NodeNumber; }NumaNode; CACHE_DESCRIPTOR Cache; ULONGLONG Reserved[2]; }; } SYSTEM_LOGICAL_PROCESSOR_INFORMATION, *PSYSTEM_LOGICAL_PROCESSOR_INFORMATION;ProcessorMask是一个位掩码,每个位代表一个逻辑处理器,第二个参数是一个标志,表示该使用第三个共用体中的哪一个结构体,这三个分别表示核心处理器,NUMA节点,以及高速缓存的信息。下面是使用的一个例子代码:#include <windows.h> #include <tchar.h> #include <stdio.h> #include <locale.h> DWORD CountBits(ULONG_PTR uMask); typedef BOOL (WINAPI *CPUINFO)(PSYSTEM_LOGICAL_PROCESSOR_INFORMATION Buffer, PDWORD ReturnLength); int _tmain(int argc, TCHAR *argv[]) { _tsetlocale(LC_ALL, _T("chs")); CPUINFO pGetCpuInfo = (CPUINFO)GetProcAddress(GetModuleHandle(_T("kernel32")), "GetLogicalProcessorInformation"); if (NULL == pGetCpuInfo) { _tprintf(_T("系统不支持该函数,程序结束\n")); return 0; } PSYSTEM_LOGICAL_PROCESSOR_INFORMATION plpt = NULL; DWORD dwLength = 0; if (!pGetCpuInfo(plpt, &dwLength)) { if (ERROR_INSUFFICIENT_BUFFER == GetLastError()) { plpt = (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLength); }else { _tprintf(_T("函数调用失败\n")); return 0; } } if (!pGetCpuInfo(plpt, &dwLength)) { _tprintf(_T("函数调用失败\n")); return 0; } DWORD dwSize = dwLength / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION); PSYSTEM_LOGICAL_PROCESSOR_INFORMATION pOffsite = plpt; DWORD dwCacheCnt = 0; DWORD dwCoreCnt = 0; DWORD dwPackageCnt = 0; DWORD dwNodeCnt = 0; for (int i = 0; i < dwSize; i++) { switch (pOffsite->Relationship) { case RelationCache: dwCacheCnt++; break; case RelationNumaNode: dwNodeCnt++; break; case RelationProcessorCore: { if (pOffsite->ProcessorCore.Flags) { dwCoreCnt++; }else { dwCoreCnt += CountBits(pOffsite->ProcessorMask); } } break; case RelationProcessorPackage: dwPackageCnt++; break; default: break; } pOffsite++; } _tprintf(_T("处理器个数:%d\n"), dwPackageCnt); _tprintf(_T("处理器核数:%d\n"), dwCoreCnt); _tprintf(_T("NUMA个数:%d\n"), dwNodeCnt); _tprintf(_T("高速缓存的个数:%d\n"), dwCacheCnt); return 0; } DWORD CountBits(ULONG_PTR uMask) { DWORD dwTest = 1; DWORD LSHIFT = sizeof(ULONG_PTR) * 8 -1; dwTest = dwTest << LSHIFT; DWORD dwCnt = 0; for (int i = 0; i <= LSHIFT; i++) { dwCnt += ((uMask & dwTest) ? 1 : 0); dwTest /= 2; } return dwCnt; }这个程序首先采用两次调用的方式分配一个合适的缓冲区,用来接收函数的返回,然后分别统计CPU数目,物理处理器数目以及逻辑处理器数量。我们根据MSDN上面得到的信息,首先判断缓冲区中有多少个结构体,也就是由多少条处理器的信息,然后根据第二个成员的值,来判断当前存储的是哪种信息。并将对应的值加1,当计算逻辑处理器的数目时需要考虑超线程的问题,所谓超线程就是intel提供的一个新的技术,可以将一个处理器虚拟成多个处理器来使用,已达到多核处理器的效果,如果它支持超线程,那么久不能简单的根据是否为核心处理器而加1,这个时候需要采用计算位掩码的方式来统计逻辑存储器,根据MSDN上说的当flag为1时表示支持超线程。windows下的进程windows中进程是已装入内存中,准备或者已经在执行的程序,磁盘上的exe文件虽说可以执行,但是它只是一个文件,并不是进程,一旦它被系统加载到内存中,系统为它分配了资源,那么它就是一个进程。进程由两个部分组成,一个是系统内核用来管理进程的内核对象,一个是它所占的地址空间。windows下的进程主要分为3大类:控制台,窗口应用,服务程序。写过控制台与窗口程序的人都知道,控制台的主函数是main,而窗口应用的主函数是WinMain,那么是否可以根据这个来判断程序属于那种呢,很遗憾,windows并不是根据这个来区分的。在VS编译器上可以通过设置将Win32 控制台程序的主函数指定为WinMain,或者将窗口程序的主函数指定为main,设置方法:属性-->连接器-->系统-->子系统,将这项设置为/SUBSYSTEM:CONSOLE,那么它的主函数为main,设置为/SUBSYSTEM:WINDOWS那么它的主函数为WinMain,甚至我们可以自己设置主函数。我们知道在C/C++语言中main程序是从main函数开始的,但是这个函数只是语法上的开始,并不是真正意义上的入口,在VC++中,系统会首先调用mainCRTStartup,在这个函数中调用main或者WinMain, 这个函数主要负责对C/C++运行环境的初始化,比如堆环境或者C/C++库函数环境的初始化。如果需要自定义自己的入口,那么这些环境将得不到初始化,也就意味着我们不能使用C/C++库函数。只能使用VC++提供的API,这么做也有一定的好处,毕竟这些库函数都是在很早之前产生的,到现在来看有很多问题,有许多有严重的安全隐患,使用API可以避免这些问题。下面是一个简单的例子:#include <Windows.h> #include <tchar.h> #include <strsafe.h> #define PRINTF(...) \ {\ TCHAR szBuf[4096] = _T("");\ StringCchPrintf(szBuf, sizeof(szBuf), __VA_ARGS__);\ size_t nStrLen = 0;\ StringCchLength(szBuf, STRSAFE_MAX_CCH, &nStrLen);\ WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), szBuf, nStrLen, NULL, NULL);\ } int LHMain() { PRINTF(_T("this is a %s"), _T("test hello world\n")); return 0; }这个例子中,入口函数是LHMain我们完全使用API的方式来编写程序,如果想要做到自定义入口,那么需要进行这样的设置:属性-->高级-->入口点,在入口点中输出我们希望作为入口点的函数名称即可。入口参数各个参数的含义现在这部分主要说明main函数以及WinMain函数。int main()这是main函数的一种原型,这种原型不带入任何参数int main(int argc, char *argv[])这种原型主要接受命令行输入的参数,参数以空格隔开,第一个参数表示输入命令行参数的总数,第二个是一个字符串指针数组,每个字符串指针成员代表的是具体输入的命令以及参数,这些信息包括我们输入的程序的名称,比如我们输入test.exe -s -a -t 来启动程序,那么argc = 4 ,argv[0] = "test.exe" argv[1] = "-s" argv[2] = "-a" argv[3] = "-t"int main(int argc, char argv[], char envs[])这个原型中第一个和第二个参数的函数与上述的带有两个参数的main函数相同,多了一个表示环境变量的指针数组,它会将环境变量以”变量名=值“的形式来组织字符串。int WINAPI WinMain(HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow);函数中的WINAPI表示函数的调用约定为__stdcall的调用方式,函数的调用方式主要与参数压栈方式以及环境回收的方式有关。在这就不再说明这部分的内容,有兴趣的可以看本人另外一篇博客专门讲解了这部分的内容,点击这里hInstance:进程的实例句柄,该值是exe文件映射到虚拟地址控件的基址hPrevInstance:上一个进程的实例句柄,为了防止进程越界访问,这个是16位下的产物,在32位下,这个没有作用。lpszCmdLine:启动程序时输入的命令行参数nCmdShow:表示程序的显示方式。进程的环境变量与工作路径进程的环境变量可以通过main函数的第三个参数传入,也可以在程序中利用函数GetEnvrionmentStrings和GetEnvrionVariable获取,下面是获取进程环境变量的简答例子: setlocale(CP_ACP, "chs"); TCHAR **ppArgs = NULL; int nArgCnt = 0; ppArgs = CommandLineToArgvW(GetCommandLine(), &nArgCnt); for (int i = 0; i < nArgCnt; i++) { _tprintf(_T("%d %s\n"), i, ppArgs[i]); } HeapFree(GetProcessHeap(), 0, ppArgs);函数的第一个参数是开启这个进程需要的完整的命令行字符串,这个字符串使用函数GetCommandLine来获取,第二个参数是一个接受环境变量的字符串指针数组。函数返回数组中元素个数。一般情况下不推荐使用环境变量的方式来保存程序所需的数据,一般采用文件或者注册表的方式,但是最好的办法是采用xml文件的方式来村粗。至于进程的工作目录可以通过函数GetCurrentDirectory方法获取。进程创建在windows下进程创建采用API函数CreateProcess,该函数的原型如下:BOOL CreateProcess( LPCWSTR pszImageName, LPCWSTR pszCmdLine, LPSECURITY_ATTRIBUTES psaProcess, LPSECURITY_ATTRIBUTES psaThread, BOOL fInheritHandles, DWORD fdwCreate, LPVOID pvEnvironment, LPWSTR pszCurDir, LPSTARTUPINFOW psiStartInfo, LPPROCESS_INFORMATION pProcInfo ); 该函数参数较多,下面对这几项做重点说明,其余的请自行查看MSDN。pszImageName:表示进程对应的exe文件所在的完整路径或者相对路径pszCmdLine:启动进程传入的命令行参数,这是一个字符串类型,需要注意的是,这个命令行参数可以带程序所在的完整路径,这样就可以将第一个参数设置为NULL。参数中的安全描述符表示的是创建的子进程的安全描述符,与当前进程的安全描述符无关。fInheritHandles表示,可否被继承,这个继承关系是指父进程中的信息能否被子进程所继承psiStartInfo规定了新进程的相关启动信息,主要有这样几个重要的值:对于窗口程序: LPTSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize;这些值规定了窗口的标题和所在的屏幕位置与长高的相关信息,对于控制台程序,主要关注: HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError;标准输入、输出、以及标准错误 下面是一个创建控制台与创建窗口的简单例子: STARTUPINFO si = {0}; si.dwXSize = 400; si.dwYSize = 300; si.dwX = 10; si.dwY = 10; PROCESS_INFORMATION pi = {0}; SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES)}; //创建一个窗口应用的进程,其中szExePath表示进程所在exe文件的路径,而szAppDirectory表示exe所在的目录 CreateProcess(szExePath, szExePath, &sa, &sa, FALSE, 0, NULL, szAppDirectory, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); //启动控制台窗口,与父进程公用输入输出环境 ZeroMemory(szExePath, sizeof(TCHAR) * (MAX_PATH + 1)); StringCchPrintf(szExePath, MAX_PATH, _T("%s%s"), szAppDirectory, _T("SubConsole.exe")); ZeroMemory(&si, sizeof(STARTUPINFO)); ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); CreateProcess(szExePath, szExePath, &sa, &sa, FALSE, 0, NULL, szAppDirectory, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); //启动控制台,在新的控制台上做输入输出 ZeroMemory(&si, sizeof(STARTUPINFO)); ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); CreateProcess(szExePath, szExePath, &sa, &sa, FALSE, CREATE_NEW_CONSOLE, NULL, szAppDirectory, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread);上述程序中,对于窗口程序,在创建时没有给出特别的创建标志,窗口本身就是一个个独立的,并且我们通过指定si的部分成员指定了窗口的显示位置,而对于控制台,如果在创建时不特别指定创建的标志,那么它将与父进程共享一个输入输出控制台。为了区分子进程和父进程的输入输出,一般通过标志CREATE_NEW_CONSOLE为新进程新建一个另外的控制台。进程输入输出重定向输入输出重定向的实现可以通过函数CreateProcess在参数psiStartInfo中的HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError中指定,但是需要注意的是,在父进程中如果采用了Create之类的函数创建了输入输出对象的句柄时一定要指定他们可以被子进程所继承。下面是一个重定向的例子: //启动控制台,做输入输出重定向到文件中 TCHAR szFilePath[MAX_PATH + 1] = _T(""); //指定文件对象可以被子进程所继承,以便子进程可以使用这个内核对象句柄 sa.bInheritHandle = TRUE; ZeroMemory(&si, sizeof(STARTUPINFO)); ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); StringCchPrintf(szFilePath, MAX_PATH, _T("%s%s"), szAppDirectory, _T("input.txt")); HANDLE hInputFile = CreateFile(szFilePath, GENERIC_READ | GENERIC_WRITE, 0, &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); ZeroMemory(szFilePath, sizeof(szFilePath)); StringCchPrintf(szFilePath, MAX_PATH, _T("%s%s"), szAppDirectory, _T("output.txt")); HANDLE hOutputFile = CreateFile(szFilePath, GENERIC_READ | GENERIC_WRITE, 0, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); si.hStdInput = hInputFile; si.hStdOutput = hOutputFile; si.dwFlags = STARTF_USESTDHANDLES; CreateProcess(szExePath, szExePath,NULL, NULL, TRUE, DETACHED_PROCESS, NULL, szAppDirectory, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(hOutputFile); CloseHandle(hInputFile); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); //启动ping命令,并将输出重定向到管道中 StringCchPrintf(szExePath, MAX_PATH, _T("ping 127.0.0.1")); DWORD dwLen = 0; BYTE byte[1024] = {0}; HANDLE hReadP = NULL; HANDLE hWriteP = NULL; sa.bInheritHandle = TRUE; CreatePipe(&hReadP, &hWriteP, &sa, 1024); si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; si.hStdOutput = hWriteP; CreateProcess(NULL, szExePath, NULL, NULL, TRUE, DETACHED_PROCESS, NULL, szAppDirectory, &si, &pi); //关闭管道的写端口,不然读端口会被阻塞 CloseHandle(hWriteP); dwLen = 1000; DWORD dwRead = 0; while (ReadFile(hReadP, byte, dwLen, &dwRead, NULL)) { if ( 0 == dwRead ) { break; } //写入管道的数据为ANSI字符 printf("%s\n", (char*)byte); ZeroMemory(byte, sizeof(byte)); }进程的退出进程在遇到如下情况中的任意一种时会退出:进程中任意一个线程调用函数ExitProcess进程的主线程结束进程中最后一个线程结束调用TerminateProcess在这针对第2、3中情况作特别的说明:这两种情况看似矛盾不是吗,当主线程结束时进程就已经结束了,这个时候还会等到最后一个线程吗。其实真实的情况是主线程结束,进程结束这个限制是VC++上的,之前在自定义入口的时候说过,main函数只是语法上的,并不是实际的入口,在调用main之前会调用mainCRTStartup,这个函数会负责调用main函数,当main函数调用结束后,这个函数会隐式的调用ExitProcess结束进程,所以只有当我们自定了程序入口才会看到3所示的现象,下面的例子说明了这点:DWORD WINAPI ThreadProc(LPVOID lpParam); #define PRINT(s) \ {\ size_t sttLen = 0;\ StringCchLengthA(s, 1024, &sttLen);\ WriteConsoleA(GetStdHandle(STD_OUTPUT_HANDLE), s, sttLen, NULL, NULL);\ } int LHMain(int argc, char *argv[]) { CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); return 0; } DWORD WINAPI ThreadProc(LPVOID lpParam) { PRINT("main thread is ending,this is child thread\r\n"); return 0; }这种程序,如果我们采用系统中规定的main函数的话,是看不到线程中输出的信息的,因为主线程先结束的话,整个进程就结束了,线程还来不及输出,就被终止了。但是我们采用自定义入口的方式,屏蔽了这个特性,所以它会等到所有线程执行完成后才会结束,这个时候就会看到这句话输出了。进程在终止时会发生以下事件:关闭进程打开的对象句柄,但是对象本身不一定会关闭,这是因为每个对象都有一个计数器,每当有一个线程在使用这个对象时计数器会加1,而释放它的句柄时会减一,只有当计数器为0时才会销毁这个对象。对象是可以跨进程访问的,而且所有相同的对象在内存中只有一份,所以需要一个计数器以便在没有被任何进程访问的时候系统删除它。进程对象的状态设为有信号,以便所有等待该进程对象信号的函数(像函数WaitForSingleObject)能够正常返回。进程的终止状态从STILL_ACTIVE变成进程的退出码,可以通过这个特性判断某个进程是否在运行,具体的方式是通过函数GetExitProcess获取进程的终止码,如果函数返回STILL_ACTIVE,则说明进程仍在运行。
2016年09月06日
17 阅读
0 评论
0 点赞
2016-08-30
windows 安全模型简介
操作系统中有些资源是不能由用户代码直接访问的,比如线程进程,文件等等,这些资源必须由系统级代码由RING3层进入到RING0层操作,并且返回一些标识供用户程序使用,一般调用某个函数陷入到内核,这样的函数叫做系统调用,而有些不直接陷入到内核,一般叫做系统API,linux中使用系统调用,而windows中封装了一系列的API。windows对象与句柄windows对象操作系统为了安全,提供了一种保护机制,这种机制会禁止用户操作某些资源,避免用户过于关注细节,或者由于操作不当而造成系统崩溃。windows中将所有这些资源封装成了一个个对象。对象就是操作系统为了维护这些资源而定义的一系列的数据结构。对象这个词我们很容易联想到面向对象编程语言中的对象,对象其实就是对一些数据以及操作这些数据的一个封装,而windows是采用面向对象思想开发的,所以我们可以将windows中的对象想象成面向对象中的对象,而windows针对每种对象都提供了操作函数,这些函数一般都会以句柄作为第一个参数,这就有点像类函数中传入的this指针。而这操作对象的函数就是类函数。windows中总共有三种对象:GUI对象、GDI对象、内核对象。windows中的句柄windows中对象的操作是由系统提供的一系列的API函数来完成,这些函数有一个共同特点,就是以HANDLE 句柄作为第一个参数,windows中采用句柄来唯一标识每个内核对象。在windows中句柄的定义如下:typedef void * HANDLE从上面的定义可以看出,这个句柄应该是指向这个对象的结构体的指针。由于每次在程序启动时内存都是随机分配的,所以句柄不要使用硬编码的方式,同时在复制内核对像的时候,并不是简单的复制它的句柄,对象的复制有专门的函数,DuplicateHandle,该函数原型如下:BOOL DuplicateHandle( HANDLE hSourceProcessHandle, //源对象所在进程句柄 HANDLE hSourceHandle, //源对象句柄 HANDLE hTargetProcessHandle, //目标对象所在进程句柄 LPHANDLE lpTargetHandle, //返回目标对象的句柄 DWORD dwDesiredAccess, //以何种权限复制 BOOL bInheritHandle, //复制的对象句柄是否可继承 DWORD dwOptions //标记一些特殊的动作 );下面是一个例子代码:HANDLE hMutex = CreateMutex(NULL, FALSE, NULL); HANDLE hMutexDup, hThread; DWORD dwThreadId; DuplicateHandle(GetCurrentProcess(),hMutex,GetCurrentProcess(),&hMutexDup, 0,FALSE,DUPLICATE_SAME_ACCESS);上述代码在本进程中复制了一个互斥对象对象的句柄。windows 安全对象模型windows中的内核对象由进程和线程进行操作,而对象就好像一个被锁上的房间,进程想要访问对象,并对对象进程某种操作,就必须获取这个对象的钥匙,而线程就好像拥有钥匙的人,只有钥匙是对的,才可以访问。这把锁能够由不同的钥匙打开,这些钥匙信息存储在ACE中,而只有当ACE中的信息与访问字串这个钥匙匹配才可以打开。我们一般把这个钥匙称为访问字串(Token)访问字串访问字串主要包括:用户标识、组标识、优先权信息、以及其他访问信息。用户标识:用于唯一标识每个用户,就好像为每个用户都分配了一个唯一的用户ID组标识:用户所属组的唯一标识ID优先权:一般系统对每个用户以及它所属组分配了一些权限,而有的时候这些权限并不够,这个时候需要通过这个优先权信息额外新增一些权限当用户登录windows系统时,系统就为这个用户分配了一个带有该用户信息的访问字串,该用户创建的每个安全对象都有这个访问字串的拷贝,当用户打开的进程试图访问某个安全对象时系统就在对象的ACL中查找该用户是否有某项权限。有这个权限才能对对象进行这项操作。子进程的访问字串一般继承与父进程,但是子进程也可以自行创建访问字串来覆盖原来的访问字串。操作访问字串所使用的API主要有以下几个:OpenProcessToken //打开进程的访问令牌OpenThreadToken //打开线程的访问令牌AdjustTokenGroups //改变用户组的访问令牌AdjustTokenPrivileges //改变令牌的访问特权GetTokenInformation //获取访问令牌的信息SetTokenInformation //设置访问令牌信息下面主要介绍一下函数GetTokenInformation 的用法:BOOL WINAPI GetTokenInformation( __in HANDLE TokenHandle, //访问字串的句柄 __in TOKEN_INFORMATION_CLASS TokenInformationClass, //需要返回访问字串哪方面的信息 __out_opt LPVOID TokenInformation, //用于接收返回信息的缓冲 __in DWORD TokenInformationLength, //缓冲的长度 __out PDWORD ReturnLength //实际所需的缓冲的长度 ); 这个函数可以返回访问令牌多方面的信息,具体返回哪个方面的信息,需要通过第二个参数来指定,这个参数是一个枚举类型,表示需要获取的信息,每个方面的信息都定义了对应的结构体,这些信息可以由MSDN中查到。另外这个函数支持两次调用,第一次传入NULL指针和0长度,这样通过最后一个参数可以得到具体所需要的缓冲的大小。SID访问字串中用户与用户组采用安全标识符的方式唯一标识(Security Indentifer SID),系统中的SID是唯一的。它主要用来标识下面的这些内容:安全描述符中的所有者和用户组被ACE认可的访问者访问字串的用户和组SID的长度是可变的,在使用时不应该使用SID这个数据类型,因为这个时候还不知道需要的长度是多少,应该由系统来创建并返回它的指针,所以在使用时需要使用SID的指针。下面是一些操作SID的APIAllocateAndInitializeSid //初始化一个SIDFreeSid //释放一个SIDCopySid //拷贝一个SIDEqualSid //判断两个SID是否相等GetLengthSid //获取SID的长度IsValidSid //是否是有效的SIDConvertSidToStringSid //将SID转化为字符串的方式下面是一个利用这些API获取系统用户组的SID和当前登录用户的SID的例子:BOOL GetLoginSid(HANDLE hToken, PSID *ppsid); //SID本身大小是不可知的,所以在传入参数时应该传入2级指针,由函数自己决定大小 void FreeSid(PSID *ppsid); int _tmain(int argc, TCHAR* argv[]) { setlocale(LC_ALL, "chs"); HANDLE hProcess = GetCurrentProcess(); HANDLE hToken = NULL; OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, &hToken); PSID pSid = NULL; GetLoginSid(hToken, &pSid); LPTSTR pStringSid = NULL; ConvertSidToStringSid(pSid, &pStringSid); _tprintf(_T("当前登录的用户sid = %s\n"), pStringSid); FreeSid(&pSid); return 0; } BOOL GetLoginSid(HANDLE hToken, PSID *ppsid) { TOKEN_GROUPS *ptg = NULL; BOOL bSuccess = FALSE; DWORD dwLength = 0; if(!GetTokenInformation(hToken, TokenGroups, ptg, 0, &dwLength)) { if (ERROR_INSUFFICIENT_BUFFER != GetLastError()) { goto __CLEAN_UP; } ptg = (TOKEN_GROUPS*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLength); if (!GetTokenInformation(hToken, TokenGroups, ptg, dwLength, &dwLength)) { goto __CLEAN_UP; } } _tprintf(_T("共找到%d个组SID\n"), ptg->GroupCount); LPTSTR pStringSid = NULL; for (int i = 0; i < ptg->GroupCount; i++) { ConvertSidToStringSid(ptg->Groups[i].Sid, &pStringSid); _tprintf(_T("\t id = %d sid = %s"), i, pStringSid); if ((ptg->Groups[i].Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID) { _tprintf(_T("此用户为当前登录的用户")); *ppsid = (PSID)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLength); CopySid(dwLength, *ppsid, ptg->Groups[i].Sid); } _tprintf(_T("\n")); } __CLEAN_UP: if (ptg != NULL) { HeapFree(GetProcessHeap(), 0, ptg); } return bSuccess; } void FreeSid(PSID *ppsid) { HeapFree(GetProcessHeap(), 0, *ppsid); *ppsid = NULL; }这个例子是MSDN中的一个例子,上述代码中首先获取进程的访问令牌,然后通过函数GetTokenInformation 获取访问令牌的信息。通过传入TokenGroups这个值获取当前用户所在用户组的访问字串。并将信息保存到结构体TOKEN_GROUPS中最后通过(ptg->Groups[i].Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID这样一个表达式来判断当前的SID是否是登录用户的。优先权优先权是由字符串标识的局部唯一的标识符(LUID)优先权是由系统管理员分配给对应的用户,一般不能通过编程的方式提升用户的优先权,但是有时候即使用户具有某个优先权,但是它启动的程序并不具有相关的优先权。这个时候可以通过编程的方式提升用户进程特权。系统有3个值代表一个优先权:字符串名,整个系统上都有意义,称为全局名,这个字符串并不是一个可读的字符串,显示出来的信息不一定能看得懂。显示给用户的可读名称;如:改变系统时间(可以在组策略中查看)每个计算机都不同的局部值;下面有几个常用的优先权:#define SE_DEBUG_NAME TEXT("SeDebugPrivilege") //调试进程 #define SE_LOAD_DRIVER_NAME TEXT("SeLoadDriverPrivilege") //装载驱动 #define SE_LOCK_MEMORY_NAME TEXT("SeLockMemoryPrivilege") //锁定内存页面 #define SE_SHUTDOWN_NAME TEXT("SeShutdownPrivilege") //关机下面是几个优先权函数LookupPrivilegeValue //查询优先权的值LookupPrivilegeDisplayName //查询优先权的输出名LookupPrivilegeName //查询优先权的名称PrivilegeCheck //优先权信息检查下面是一个获取用户特权信息的代码:int _tmain(int argc, TCHAR *argv[]) { setlocale(LC_ALL, "chs"); HANDLE hToken = NULL; HANDLE hProcess = GetCurrentProcess(); OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken); TOKEN_PRIVILEGES* ptg = NULL; DWORD dwTgSize = 0; TCHAR szName[255] = _T(""); TCHAR szDisplay[255] = _T(""); DWORD languageID = GetUserDefaultLangID(); if (!GetTokenInformation(hToken, TokenPrivileges, ptg, 0, &dwTgSize)) { if (ERROR_INSUFFICIENT_BUFFER != GetLastError()) { _tprintf(_T("获取令牌失败\n")); return 0; } ptg = (TOKEN_PRIVILEGES*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwTgSize); if (!GetTokenInformation(hToken, TokenPrivileges, ptg, dwTgSize, &dwTgSize)) { _tprintf(_T("读取令牌信息失败\n")); return 0; } } _tprintf(_T("用户的特权信息:\n")); for(int i = 0; i < ptg->PrivilegeCount; i++) { DWORD dwName = sizeof(szName) / sizeof(TCHAR); DWORD dwDisplay = sizeof(szDisplay) / sizeof(TCHAR); LookupPrivilegeName(NULL, &ptg->Privileges[i].Luid, szName, &dwName); LookupPrivilegeDisplayName(NULL, szName, szDisplay, &dwDisplay, &languageID); _tprintf(_T("\t 特权名%s %s"), szName, szDisplay); if (ptg->Privileges[i].Attributes & ((SE_PRIVILEGE_ENABLED | SE_PRIVILEGE_ENABLED_BY_DEFAULT))) { _tprintf(_T("特权开放\n")); }else { _tprintf(_T("特权关闭\n")); } } HeapFree(GetProcessHeap(), 0, ptg); return 0; }下面是进程提权的代码:int _tmain(int argc, TCHAR *argv[]) { HANDLE hToken = NULL; HANDLE hProcess = GetCurrentProcess(); SetPrivileges(hToken, SE_TCB_NAME , TRUE); return 0; } BOOL SetPrivileges(HANDLE hToken, LPTSTR lpPrivilegesName, BOOL bEnablePrivilege) { //获取Token的特权信息 LUID uid = {0}; LookupPrivilegeValue(NULL, lpPrivilegesName, &uid); TOKEN_PRIVILEGES tp = {0}; tp.PrivilegeCount = 1; tp.Privileges[0].Luid = uid; if (bEnablePrivilege) { tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; }else { tp.Privileges[0].Attributes = 0; } AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL); if ( GetLastError() == ERROR_NOT_ALL_ASSIGNED ) { _tprintf(_T("分配指定的特权(%s)失败. \n"),lpPrivilegesName); return FALSE; } return TRUE; }安全描述符安全描述符中通常包含信息:所有者、主组、任选访问控制列表(DACL)、系统访问控制列表(SACL)安全描述符是以SECURITE_DESCRIPTOR结构开始,后面连续跟着安全描述符的其它信息访问控制列表访问控制列表(Access Control List ACL)主要由多个访问访问控制入口(Access Control Entries ACE)组成。ACE用于标识一个用户、组或局部组以及它们中每一个允许的访问权;安全描述符的创建在创建安全访问对象的函数中一般都需要填入一个SECURITY_ATTRIBUTES结构体的指针,我们要么给定一个NULL值使其具有默认的安全属性,或者自己创建一个安全描述符并将他的指针传入。创建一个安全描述符主要有下面几步:用函数 AllocateAndInitializeSid 创建用户的SID。函数的定义如下:BOOL WINAPI AllocateAndInitializeSid( __in PSID_IDENTIFIER_AUTHORITY pIdentifierAuthority, //用于表示该SID标识的颁发机构 __in BYTE nSubAuthorityCount, //SID有多少个子部分 __in DWORD dwSubAuthority0, //第0个子部分 __in DWORD dwSubAuthority1, __in DWORD dwSubAuthority2, __in DWORD dwSubAuthority3, __in DWORD dwSubAuthority4, __in DWORD dwSubAuthority5, __in DWORD dwSubAuthority6, __in DWORD dwSubAuthority7, __out PSID* pSid //返回一个PSID的指针 );SID主要由一个颁发机构以及一个或者多个32位的唯一的RID组成这些RID通过参数dwSubAuthority0到dwSubAuthority7生成。对应的用户SID都有固定的组合。这个我还没找到具体的用户与SID是如何定义的。为该用户SID分配访问控制权限,主要通过填充结构体EXPLICIT_ACCESS成员来实现,这个结构体的定义如下:typedef struct _EXPLICIT_ACCESS { DWORD grfAccessPermissions; //制定用户权限 ACCESS_MODE grfAccessMode; //用于表示允许、拒绝、审查特定用户的权限 DWORD grfInheritance; //当前的权限是否可以继承 TRUSTEE Trustee; //这是一个访问托管 } EXPLICIT_ACCESS, *PEXPLICIT_ACCESS;3.用API SetEntriesInAcl将上述结构体放入到ACL中分配并初始化SECURITY_DESCRIPTOR结构体,初始化该结构体所用到的API 是InitializeSecurityDescriptor将SECURITY_DESCRIPTOR结构加入到安全描述符中,安全描述符的结构体是:SECURITY_ATTRIBUTES下面是一个具体的例子(这个例子来自MSDN): SID_IDENTIFIER_AUTHORITY SIDAuthorityNT = SECURITY_WORLD_SID_AUTHORITY; SID_IDENTIFIER_AUTHORITY SIDAuthorityWord = SECURITY_NT_AUTHORITY; PSID pEveryOneSid = NULL; PSID pAdminSid = NULL; EXPLICIT_ACCESS ea[2] = {0}; PACL pAcl = NULL; //创建everyone用户的SID AllocateAndInitializeSid(&SIDAuthorityWord, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &pEveryOneSid); PSECURITY_DESCRIPTOR pSD = NULL; //定义everyone 的访问控制信息 ea[0].grfAccessMode = SET_ACCESS; //用于表示允许、拒绝、审查特定用户的权限 ea[0].grfAccessPermissions = KEY_READ; //制定用户权限 ea[0].grfInheritance = FALSE; ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; ea[0].Trustee.ptstrName = (LPTSTR)pEveryOneSid; //创建administor用户组的SID AllocateAndInitializeSid(&SIDAuthorityNT, 2, SECURITY_BUILTIN_DOMAIN_RID,DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pAdminSid); ea[1].grfAccessMode = SET_ACCESS; ea[1].grfAccessPermissions = KEY_ALL_ACCESS; ea[1].grfInheritance = FALSE; ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID; ea[1].Trustee.TrusteeType = TRUSTEE_IS_GROUP; ea[1].Trustee.ptstrName = (LPTSTR)pAdminSid; //将以上两个SID加入到ACL中 SetEntriesInAcl(2, ea, NULL, &pAcl); pSD = (PSECURITY_DESCRIPTOR) HeapAlloc(GetProcessHeap(), 0, SECURITY_DESCRIPTOR_MIN_LENGTH); //初始化一个SECURITY_DESCRIPTOR结构 InitializeSecurityDescriptor(&pSD, SECURITY_DESCRIPTOR_REVISION); //将SECURITY_DESCRIPTOR结构加入到安全描述符中 SECURITY_ATTRIBUTES sa = {0}; sa.bInheritHandle = FALSE; sa.lpSecurityDescriptor = pSD; sa.nLength = sizeof(SECURITY_ATTRIBUTES); HKEY hkSub = NULL; DWORD dwDisposition = 0; RegCreateKeyEx(HKEY_CURRENT_USER, _T("mykey"), 0, _T(""), 0, KEY_READ | KEY_WRITE, &sa, &hkSub, &dwDisposition); if (pEveryOneSid) { FreeSid(pEveryOneSid); } if (pAdminSid) { FreeSid(pAdminSid); } if (pAcl) { LocalFree(pAcl); } HeapFree(GetProcessHeap(), 0, pSD); if (hkSub) { RegCloseKey(hkSub); }
2016年08月30日
10 阅读
0 评论
0 点赞
2016-08-14
windows平台调用函数堆栈的追踪方法
在windows平台,有一个简单的方法来追踪调用函数的堆栈,就是利用函数CaptureStackBackTrace,但是这个函数不能得到具体调用函数的名称,只能得到地址,当然我们可以通过反汇编的方式通过地址得到函数的名称,以及具体调用的反汇编代码,但是对于有的时候我们需要直接得到函数的名称,这个时候据不能使用这个方法,对于这种需求我们可以使用函数:SymInitialize、StackWalk、SymGetSymFromAddr、SymGetLineFromAddr、SymCleanup。原理基本上所有高级语言都有专门为函数准备的堆栈,用来存储函数中定义的变量,在C/C++中在调用函数之前会保存当前函数的相关环境,在调用函数时首先进行参数压栈,然后call指令将当前eip的值压入堆栈中,然后调用函数,函数首先会将自身堆栈的栈底地址保存在ebp中,然后抬高esp并初始化本身的堆栈,通过多次调用最终在堆栈段形成这样的布局这里对函数的原理做简单的介绍,有兴趣的可以看我的另一篇关于C函数原理讲解的博客,点击这里跳转VC++编译器在编译时对函数名称与地址都有详细的记录,编译出来的程序都有一个符号常量表,将符号常量与它对应的地址形成映射,在搜索时首先根据这些堆栈环境找到对应地址,然后根据地址在符号常量表中,找到具体调用的信息,这是一个很复杂的工程,需要对编译原理和汇编有很强的基础,幸运的是,如今这些工作不需要程序员自己去做,windows帮助我们分配了一组API,在编写程序时只需要调用API即可函数说明SymInitialize:这个函数主要用作初始化相关环境。SymCleanup:清楚这个初始化的相关环境,在调用SymInitialize之后需要调用SymCleanup,进行释放资源的操作StackWalk:程序的功能主要由这个函数实现,函数会从初始化时的堆栈顶开始向下查找下一个堆栈的信息,原型如下:BOOL WINAPI StackWalk( __in DWORD MachineType, //机器类型现在一般是intel的x86系列,这个时候填入IMAGE_FILE_MACHINE_I386 __in HANDLE hProcess, //追踪的进程句柄 __in HANDLE hThread, //追踪的线程句柄 __in_out LPSTACKFRAME StackFrame, //记录的追踪到的堆栈信息 __in_out PVOID ContextRecord, //记录当前的线程环境 __in PREAD_PROCESS_MEMORY_ROUTINE ReadMemoryRoutine, __in PFUNCTION_TABLE_ACCESS_ROUTINE FunctionTableAccessRoutine, __in PGET_MODULE_BASE_ROUTINE GetModuleBaseRoutine, __in PTRANSLATE_ADDRESS_ROUTINE TranslateAddress //后面的四个参数都是回掉函数,有系统自行调用,而且这些函数都是定义好的,只需要填入相应的函数名称 );需要注意的一点是,在首次调用该函数时需要对StackFrame中的AddrPC、AddrFrame、AddrStack这三个成员进行初始化,填入相关值,以便函数从此处线程堆栈的栈顶进行搜索,否则调用函数将失败,具体如何填写请看MSDN。SymGetSymFromAddr:根据获取到的函数地址得到函数名称、堆栈大小等信息,这个函数的原型如下: BOOL WINAPI SymGetSymFromAddr( __in HANDLE hProcess, //进程句柄 __in DWORD Address, //函数地址 __out PDWORD Displacement, //返回该符号常量的位移或者填入NULL,不获取此值 __out PIMAGEHLP_SYMBOL Symbol//返回堆栈信息 );SymGetLineFromAddr:根据得到的地址值,获取调用函数的相关信息。主要记录是在哪个文件,哪行调用了该函数,下面是函数原型:BOOL WINAPI SymGetLineFromAddr( __in HANDLE hProcess, __in DWORD dwAddr, __out PDWORD pdwDisplacement, __out PIMAGEHLP_LINE Line );它参数的含义与SymGetSymFromAddr,相同。通过上面对函数的说明,我们可以知道,为了追踪函数调用的详细信息,大致步骤如下:首先调用函数SymInitialize进行相关的初始化工作。填充结构体StackFrame的相关信息,确定从何处开始追踪。循环调用StackWalk函数,从指定位置,向下一直追踪到最后。每次将获取的地址分别传入SymGetSymFromAddr、SymGetLineFromAddr,得到函数的详细信息调用SymCleanup,结束追踪但是需要注意的一点是,函数StackWalk会顺着线程堆栈进行查找,如果在调用之前,某个函数已经返回了,它的堆栈被回收,那么函数StackWalk自然不会追踪到该函数的调用。具体实现void InitTrack() { g_hHandle = GetCurrentProcess(); SymInitialize(g_hHandle, NULL, TRUE); } void StackTrack() { g_hThread = GetCurrentThread(); STACKFRAME sf = { 0 }; sf.AddrPC.Offset = g_context.Eip; sf.AddrPC.Mode = AddrModeFlat; sf.AddrFrame.Offset = g_context.Ebp; sf.AddrFrame.Mode = AddrModeFlat; sf.AddrStack.Offset = g_context.Esp; sf.AddrStack.Mode = AddrModeFlat; typedef struct tag_SYMBOL_INFO { IMAGEHLP_SYMBOL symInfo; TCHAR szBuffer[MAX_PATH]; } SYMBOL_INFO, *LPSYMBOL_INFO; DWORD dwDisplament = 0; SYMBOL_INFO stack_info = { 0 }; PIMAGEHLP_SYMBOL pSym = (PIMAGEHLP_SYMBOL)&stack_info; pSym->SizeOfStruct = sizeof(IMAGEHLP_SYMBOL); pSym->MaxNameLength = sizeof(SYMBOL_INFO) - offsetof(SYMBOL_INFO, symInfo.Name); IMAGEHLP_LINE ImageLine = { 0 }; ImageLine.SizeOfStruct = sizeof(IMAGEHLP_LINE); while (StackWalk(IMAGE_FILE_MACHINE_I386, g_hHandle, g_hThread, &sf, &g_context, NULL, SymFunctionTableAccess, SymGetModuleBase, NULL)) { SymGetSymFromAddr(g_hHandle, sf.AddrPC.Offset, &dwDisplament, pSym); SymGetLineFromAddr(g_hHandle, sf.AddrPC.Offset, &dwDisplament, &ImageLine); printf("当前调用函数 : %08x+%s(FILE[%s]LINE[%d])\n", pSym->Address, pSym->Name, ImageLine.FileName, ImageLine.LineNumber); } } void UninitTrack() { SymCleanup(g_hHandle); }测试程序如下:void func1() { OPEN_STACK_TRACK; } void func2() { func1(); } void func3() { func2(); } void func4() { printf("hello\n"); } int _tmain(int argc, TCHAR* argv[]) { func4(); func3(); func3(); return 0; }OPEN_STACK_TRACK是一个宏,它的定义如下:#define OPEN_STACK_TRACK\ HANDLE hThread = GetCurrentThread();\ GetThreadContext(hThread, &g_context);\ __asm{call $ + 5}\ __asm{pop eax}\ __asm{mov g_context.Eip, eax}\ __asm{mov g_context.Ebp, ebp}\ __asm{mov g_context.Esp, esp}\ InitTrack();\ StackTrack();\ UninitTrack();这个程序需要注意以下几点:如果想要追踪所有调用的函数,需要将这个宏放置到最后调用的位置,当然前提是此时之前被调函数的堆栈仍然存在。当然可以在调用前简单的计算,找出在哪个位置是所有函数都没有调用完成的,不过这样可能就与程序的初衷相悖,毕竟程序本身就是为了获取堆栈的调用信息。。。。IMAGEHLP_SYMBOL的结构体中关于Name的成员,只有一个字节,而函数SymGetSymFromAddr在填入值时是没有关心这个实际大小,它只是简单的填充,这就造成了缓冲区溢出的情况,为了避免我们需要在Name后面额外给一定大小的缓冲区,用来接收数据,这也就是我们定义这个结构体SYMBOL_INFO的原因。另外IMAGEHLP_SYMBOL中的MaxNameLength成员是指Name的最大长度,需要根据给定的缓冲区,进行计算。从测试程序来看,在进行追踪时func4已经调用完成,而我们在获取线程的运行时环境g_context时函数GetThreadContext,也在堆栈中,最终得到的结果中必然包含GetThreadContext的调用信息,如果想去掉这个信息,只需要修改获得信息的值,既然函数StackWalk是根据堆栈进行追踪,那么只需要修改对应堆栈的信息即可,需要修改eip 、ebp、esp的值,关于esp ebp的值很好修改,可以在对应函数中esp ebp这些寄存器的值,而eip的值就不那么好获取,本生利用mov指令得到eip的值它也是指令,会改变eip的值,从而造成获取到的eip的值不准确,所以我们利用call指令,先保存当前eip的值到堆栈,然后再从堆栈中取出。call指令的实质是 push eip和jmp addr指令的组合,并不一定非要调用函数。call指令的大小为5个字节,所以call $ + 5表示先保存eip在跳转到它的下一跳指令处。这样就可以有效的避免检测到GetThreadContext中的相关函数调用。
2016年08月14日
17 阅读
0 评论
0 点赞
2016-08-10
windows错误处理
在调用windows API时函数会首先对我们传入的参数进行校验,然后执行,如果出现什么情况导致函数执行出错,有的函数可以通过返回值来判断函数是否出错,比如对于返回句柄的函数如果返回NULL 或者INVALID_HANDLE_VALUE,则函数出错,对于返回指针的函数来说如果返回NULL则函数出错,但是对于有的函数从返回值来看根本不知道是否成功,或者为什么失败,对此windows提供了一大堆的错误码,用于标识API函数是否出错以及出错原因。在windows中为每个线程准备了一个存储区,专门用来存储当前API执行的错误码,想要获取这个错误码可以通过函数GetLastError。在这需要注意的是当前API执行返回的错误码会覆盖之前API返回的错误码,所以在调用API结束后需要立马调用GetLastError来获取该函数返回的错误码。但是windows中的错误码实在太多,有的时候错误码并不直观,windows为每个错误码都关联了一个错误信息的文本,想要通过错误码获取对应的文本信息,可以通过函数FormatMessage来获取。下面是一个具体的例子:#include <windows.h> #include <tchar.h> #include <stdio.h> #include <strsafe.h> #define GRS_OUTPUT(s) WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), s, _tcsclen(s), NULL, NULL) int _tmain(int argc, TCHAR *argv[]) { if (INVALID_HANDLE_VALUE == CreateFile(_T("C:\\Test.txt"), GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)) { LPTSTR lpMsg = NULL; DWORD dwLastError = GetLastError(); FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastError, GetUserDefaultLangID(), (LPTSTR)&lpMsg, 0, NULL); if (NULL != lpMsg) { TCHAR szErrorInfo[1024] = {0}; StringCchPrintf(szErrorInfo, sizeof(szErrorInfo), _T("打开文件失败,失败原因为:%s"), lpMsg); GRS_OUTPUT(szErrorInfo); HeapFree(GetProcessHeap(), 0, lpMsg); } } return 0; }在这段代码中我们没有使用C标准库中的printf,而是使用了windows自带的控制台函数WriteConsole,为了简单,我们定义了一个宏,用来输出字符串。函数WriteConsole的原型如下:BOOL WINAPI WriteConsole( __in HANDLE hConsoleOutput, __in const VOID* lpBuffer, __in DWORD nNumberOfCharsToWrite, __out LPDWORD lpNumberOfCharsWritten, LPVOID lpReserved );函数的第一个参数是控制台的句柄,可以通过函数GetStdHandle来获取,这个函数主要传入一个标志,表示需要获取哪个控制台的句柄,主要有:STD_INPUT_HANDLE(标准输入)、STD_OUTPUT_HANDLE(标准输出)、STD_ERROR_HANDLE(标准错误)第二个参数是字符串的指针,第三个参数是字符个数,第四个参数是实际写入字符个数,由函数返回,如果不关心可以给NULL,最后一个windows作为保留参数通常给NULL。程序首先以打开已存在文件的方式打开一个文件,由于这个文件并不存在,所以函数出错,我们通过GetLastError获取错误码,然后通过FormatMessage来进行转化,该函数原型如下:DWORD FormatMessage( DWORD dwFlags, //标志 LPCVOID lpSource, //根据第一个参数的不同而有不同的解释 DWORD dwMessageId, //错误码 DWORD dwLanguageId, //语言ID LPTSTR lpBuffer, //字符缓冲区,用来存放最终生成的格式字符串 DWORD nSize, //缓冲区大小 va_list* Arguments//作为不定参数类似于printf函数格式化字符串后面的参数 ); 第一个参数是标志,在这我们传入FORMAT_MESSAGE_ALLOCATE_BUFFER,表示字符串缓冲区由该函数为我们分配,而不用自己分配,这个时候为了接受返回的字符缓冲区指针,需要使用二级指针。传入FORMAT_MESSAGE_IGNORE_INSERTS表示忽略插入的信息,也就是说不需要进行sprintf那样的格式化字符串的操作,传入FORMAT_MESSAGE_FROM_SYSTEM表示错误信息的字符串来自于系统定义的。然后进行简单的格式化之后输出错误字符串,最后需要释放内存,虽然FormatMessage函数帮我们分陪了缓冲,但是它不负责释放,需要我们自行释放。另外我们也可以自行进行错误码的设置,利用函数SetLastError可以达到这个效果,以模拟API调用时返回错误码的操作。在windows上一般遵循这样的格式:|位|31~30|29|28|27~16|15~0||:-|:----|:-|:-|:----|:---||用途|严重性|系统错误码|保留位|设备码|异常代码||含义|0 成功 <br/>1供参考<br/>2警告<br/>3错误|0系统定义<br/>1自定义|总为0|系统设备码|具体错误码|除了获取错误信息之外,还可以获取调用堆栈的快照,可以用函数CaptureStackBackTrace获取,只是这个函数只能获取调用堆栈的线性地址,不能获取到具体的函数名称。下面是它具体的一个例子: const int nCount = 128; PVOID BackTrace[nCount] = {NULL}; int iCnt = CaptureStackBackTrace(0, nCount, BackTrace, NULL); for (int i = 0; i < iCnt; i++) { printf("调用堆栈索引%d, 函数地址:0x%08x\n", i, BackTrace[i]); } return 0;这段代码非常简短,函数只需要四个参数,第一个参数是表示从当前栈顶开始的第几个栈开始便利,第二个参数是共便利多少个栈信息,第三个参数是一个缓冲区,用来存储得到的栈信息,具体就是栈的地址。第四个参数是一个哈希数组,由函数本身返回,如果不需要这个可以设置为NULL。
2016年08月10日
13 阅读
0 评论
0 点赞
2016-07-21
windows 堆管理
windows堆管理是建立在虚拟内存管理的基础之上的,每个进程都有独立的4GB的虚拟地址空间,其中有2GB的属于用户区,保存的是用户程序的数据和代码,而系统在装载程序时会将这部分内存划分为4个段从低地址到高地址依次为静态存储区,代码段,堆段和栈段,其中堆的生长方向是从低地址到高地址,而栈的生长方向是从高地址到低地址。程序申请堆内存时,系统会在虚拟内存的基础上分配一段内存,然后记录下来这块的大小和首地址,并且在对应内存块的首尾位置各有相应的数据结构,所以在堆内存上如果发生缓冲区溢出的话,会造成程序崩溃,这部分没有硬件支持,所有管理算法都有开发者自己设计实现。堆内存管理的函数主要有HeapCreate、HeapAlloc、HeapFree、HeapRealloc、HeapDestroy、HeapWalk、HeapLock、HeapUnLock。下面主要通过一些具体的操作来说明这些函数的用法。堆内存的分配与释放堆内存的分配主要用到函数HeapAlloc,下面是这个函数的原型:LPVOID HeapAlloc( HANDLE hHeap, //堆句柄,表示在哪个堆上分配内存 DWORD dwFlags, //分配的内存的相关标志 DWORD dwBytes //大小 );堆句柄可以使用进程默认堆也可以使用用户自定义的堆,自定义堆使用函数HeapCreate,函数返回堆的句柄,使用GetProcessHeap可以获取系统默认堆,返回的也是一个堆句柄。分配内存的相关标志有这样几个值:HEAP_NO_SERIALIZE:这个表示对堆内存不进行线程并发控制,由于系统默认会进行堆的并发控制,防止多个线程同时分配到了同一个堆内存,如果程序是单线程程序则可以添加这个选项,适当提高程序运行效率。HEAP_ZERO_MEMORY:这个标志表示在分配内存的时候同时将这块内存清零。HeapCreate函数的原型如下:HANDLE HeapCreate( DWORD flOptions, //堆的相关属性 DWORD dwInitialSize, //堆初始大小 DWORD dwMaximumSize //堆所占内存的最大值 );flOptions的取值如下:HEAP_NO_SERIALIZE:取消并发控制HEAP_SHARED_READONLY:其他进程可以以只读属性访问这个堆dwInitialSize, dwMaximumSize这两个值如果都是0,那么堆内存的初始大小由系统分配,并且堆没有上限,会根据具体的需求而增长。下面是使用的例子: //在系统默认堆中分配内存 srand((unsigned int)time(NULL)); HANDLE hHeap = GetProcessHeap(); int nCount = 1000; float *pfArray = (float *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY | HEAP_NO_SERIALIZE, nCount * sizeof(float)); for (int i = 0; i < nCount; i++) { pfArray[i] = 1.0f * rand(); } HeapFree(hHeap, HEAP_NO_SERIALIZE, pfArray); //在自定义堆中分配内存 hHeap = HeapCreate(HEAP_GENERATE_EXCEPTIONS, 0, 0); pfArray = (float *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY | HEAP_NO_SERIALIZE, nCount * sizeof(float)); for (int i = 0; i < nCount; i++) { pfArray[i] = 1.0f * rand(); } HeapFree(hHeap, HEAP_NO_SERIALIZE, pfArray); HeapDestroy(hHeap);遍历进程中所有堆的信息:便利堆的信息主要用到函数HeapWalk,该函数的原型如下:BOOL WINAPI HeapWalk( __in HANDLE hHeap,//堆的句柄 __in_out LPPROCESS_HEAP_ENTRY lpEntry//返回堆内存的相关信息 );下面是PROCESS_HEAP_ENTRY的原型:typedef struct _PROCESS_HEAP_ENTRY { PVOID lpData; DWORD cbData; BYTE cbOverhead; BYTE iRegionIndex; WORD wFlags; union { struct { HANDLE hMem; DWORD dwReserved[3]; } Block; struct { DWORD dwCommittedSize; DWORD dwUnCommittedSize; LPVOID lpFirstBlock; LPVOID lpLastBlock; } Region; }; } PROCESS_HEAP_ENTRY, *LPPROCESS_HEAP_ENTRY;这个结构中的公用体具体使用哪个与wFlags相关,下面是这些值得具体含义:wFlags堆入口含义lpDatacbDatacbOverhead块前堆数据结构大小iRegionIndexBlockRegionPROCESS_HEAP_ENTRY_BUSY被分配的内存块首地址内存块大小内存块前堆数据结构所在区域索引无意义无意义PROCESS_HEAP_ENTRY_DDESHAREDDE共享内存块首地址内存块大小内存块前堆数据结构所在区域索引无意义无意义PROCESS_HEAP_ENTRY_MOVEABLE可移动的内存块(兼容GlobalAllocLocalAlloc)首地址(可移动内存句柄的首地址)内存块大小内存块前堆数据结构所在区域索引与PROCESS_HEAP_ENTRY_BUSY标志一同指定可移动内存句柄值无意义PROCESS_HEAP_REGION已提交的堆虚拟内存区域区域开始地址区域大小区域前堆数据结构区域索引无意义虚拟内存区域详细信息PROCESS_HEAP_UNCOMMITTED_RANGE未提交的堆虚拟内存区域区域开始地址区域大小区域前堆数据结构区域索引无意义无意义下面是时遍历堆内存的例子: PHANDLE pHeaps = NULL; //当传入的参数为0和NULL时,函数返回进程中堆的个数 int nCount = GetProcessHeaps(0, NULL); pHeaps = new HANDLE[nCount]; //获取进程所有堆句柄 GetProcessHeaps(nCount, pHeaps); PROCESS_HEAP_ENTRY phe = {0}; for (int i = 0; i < nCount; i++) { cout << "Heap handle: 0x" << pHeaps[i] << '\n'; //在读取堆中的相关信息时需要将堆内存锁定,防止程序向堆中写入数据 HeapLock(pHeaps[i]); HeapWalk(pHeaps[i], &phe); //输出堆信息 cout << "\tSize: " << phe.cbData << " - Overhead: " << static_cast<DWORD>(phe.cbOverhead) << '\n'; cout << "\tBlock is a"; if(phe.wFlags & PROCESS_HEAP_REGION) { cout << " VMem region:\n"; cout << "\tCommitted size: " << phe.Region.dwCommittedSize << '\n'; cout << "\tUncomitted size: " << phe.Region.dwUnCommittedSize << '\n'; cout << "\tFirst block: 0x" << phe.Region.lpFirstBlock << '\n'; cout << "\tLast block: 0x" << phe.Region.lpLastBlock << '\n'; } else { if(phe.wFlags & PROCESS_HEAP_UNCOMMITTED_RANGE) { cout << "n uncommitted range\n"; } else if(phe.wFlags & PROCESS_HEAP_ENTRY_BUSY) { cout << "n Allocated range: Region index - " << static_cast<unsigned>(phe.iRegionIndex) << '\n'; if(phe.wFlags & PROCESS_HEAP_ENTRY_MOVEABLE) { cout << "\tMovable: Handle is 0x" << phe.Block.hMem << '\n'; } else if(phe.wFlags & PROCESS_HEAP_ENTRY_DDESHARE) { cout << "\tDDE Sharable\n"; } } else cout << " block, no other flags specified\n"; } cout << std::endl; HeapUnlock(pHeaps[i]); ZeroMemory(&phe, sizeof(PROCESS_HEAP_ENTRY)); } delete[] pHeaps;另外堆还有其他操作,比如使用HeapSize获取分配的内存大小,使用HeapValidate可以校验一个对内存的完整性,从而提早发现”野指针”等等。
2016年07月21日
21 阅读
0 评论
0 点赞
2016-07-21
windows虚拟内存管理
内存管理是操作系统非常重要的部分,处理器每一次的升级都会给内存管理方式带来巨大的变化,向早期的8086cpu的分段式管理,到后来的80x86 系列的32位cpu推出的保护模式和段页式管理。在应用程序中我们无时不刻不在和内存打交道,我们总在不经意间的进行堆内存和栈内存的分配释放,所以内存是我们进行程序设计必不可少的部分。CPU的内存管理方式段寄存器怎么消失了?在学习8086汇编语言时经常与寄存器打交道,其中8086CPU采用的内存管理方式为分段管理的方式,寻址时采用:短地址 * 16 + 偏移地址的方式,其中有几大段寄存器比如:CS、DS、SS、ES等等,每个段的偏移地址最大为64K,这样总共能寻址到2M的内存。但是到32位CPU之后偏移地址变成了32位这样每个段就可以有4GB的内存空间,这个空间已经足够大了,这个时候在编写相应的汇编程序时我们发现没有段寄存器的身影了,是不是在32位中已经没有段寄存器了呢,答案是否定了,32位CPU中不仅有段寄存器而且它们的作用比以前更大了。在32位CPU中段寄存器不再作为段首地址,而是作为段选择子,CPU为了管理内存,将某些连续的地址内存作为一页,利用一个数据结构来说明这页的属性,比如是否可读写,大小,起始地址等等,这个数据结构叫做段描述符,而多个段描述符则组成了一个段描述符表,而段寄存器如今是用来找到对应的段描述符的,叫做段选择子。段寄存器仍然是16位其中高13位表示段描述符表的索引,第二位是区分LDT(局部描述符表)和GDT(全局描述符表),全局描述符表是系统级的而LDT是每个进程所独有的,如果第二位表示的是LDT,那么首先要从GDT中查询到LDT所在位置,然后才根据索引找到对应的内存地址,所以现在寻址采用的是通过段选择子查表的方式得到一个32位的内存地址。由于这些表都是由系统维护,并且不允许用户访问及修改所以在普通应用程序中没有必要也不能使用段寄存器。通过上面的说明,我们可以推导出来32位机器最多可以支持2^(13 + 1 + 32) = 64T内存。段页式管理通过查表方式得到的32位内存地址是否就是真实的物理内存的地址呢,这个也是不一定的,这个还要看系统是否开启了段页式管理。如果没有则这个就是真实的物理地址,如果开启了段页式管理那么这个只是一个线性地址,还需要通过页表来寻址到真实的物理内存。32位CPU专门新赠了一个CR3寄存器用来完成分页式管理,通过CR3寄存器可以寻址到页目录表,然后再将32位线性地址的高10位作为页目录表的索引,通过这个索引可以找到相应的页表,再将中间10为作为页表的索引,通过这个索引可以寻址到对应物理内存的起始地址,最后通过这个其实地址和最后低12位的偏移地址找到对应真实内存。下面是这个过程的一个示例图:为什么要使用分页式管理,直接让那个32位线性地址对应真实的内存不可以吗。当然可以,但是分页式管理也有它自身的优点:可以实现页面的保护:系统通过设置相关属性信息来指定特权级别和其他状态可以实现物理内存的共享:从上面的图中可以看出,不同的线性地址是可以映射到相同的物理内存上的,只需要更改页表中对应的物理地址就可以实现不同的线性地址对应相同的物理内存实现内存共享。可以方便的实现虚拟内存的支持:在系统中有一个pagefile.sys的交互页面文件,这个是系统用来进行内存页面与磁盘进行交互,以应对内存不够的情况。系统为每个内存页维护了一个值,这个值表示该页面多久未被访问,当页面被访问这个值被清零,否则每过一段时间会累加一次。当这个值到达某个阈值时,系统将页面中的内容放入磁盘中,将这块内存空余出来以便保存其他数据,同时将之前的线性地址做一个标记,表名这个线性地址没有对应到具体的内存中,当程序需要再次访问这个线性地址所对应的内存时系统会再次将磁盘中的数据写入到内存中。虽说这样做相当于扩大了物理内存,但是磁盘相对于内存来说是一个慢速设备,在内存和磁盘间进行数据交换总是会耗费大量的时间,这样会拖慢程序运行,而采用SSD硬盘会显著提高系统运行效率,就在于SSD提高了与内存进行数据交换的效率。如果想显著提高效率,最好的办法是加内存毕竟在内存和硬盘间倒换数据是要话费时间的。保护模式在以前的16位CPU中采用的多是实模式,程序中使用的地址都是真实的物理地址,这样如果内存分配不合理,会造成一个程序将另外一个程序所在的内存覆盖这样对另外一个程序将造成严重影响,但是在32位保护模式下,不再会产生这种问题,保护模式将每个进程的地址空间隔离开来,还记得上面的LDT吗,在不同的程序中即使采用的是相同的地址,也会被LDT映射到不同的线性地址上。保护模式主要体现在这样几个方面:1.同一进程中,使用4个不同访问级别的内存段,对每个页面的访问属性做了相应的规定,防止错误访问的情况,同时为提供了4中不同代码特权,0特权的代码可以访问任意级别的内存,1特权能任意访问1...3级内存,但不能访问0级内存,依次类推。通常这些特权级别叫做ring0-ring3。对于不同的进程,将他们所用到的内存等资源隔离开来,一个进程的执行不会影响到另一个进程。windows系统的内存管理windows内存管理器我们将系统中实际映射到具体的实际内存上的页面称为工作集。当进程想访问多余实际物理内存的内存时,系统会启用虚拟内存管理机制(工作集管理),将那些长时间未访问的物理页面复制到硬盘缓冲文件上,并释放这些物理页面,映射到虚拟空间的其它页面上;系统的内存管理器主要由下面的几个部分组成:工作集管理器(优先级16):这个主要负责记录每个页面的年龄,也就有多久未被访问,当页面被访问这个年龄被清零,否则每过一段时间就进行累加1的操作。进程/栈交换器(优先级23):主要用于在进行进程或者线程切换时保存寄存器中的相关数据用以保存相关环境。已修改页面写出器(优先级17):当内存映射的内容发生改变时将这个改变及时的写入到硬盘中,防止由于程序意外终止而造成数据丢失映射页面写出器(优先级17):当页面的年龄达到一定的阈值时,将页面内容写入到硬盘中解引用段线程(优先级18):释放以写入到硬盘中的空闲页面零页面线程(优先级0):将空闲页面清零,以便程序下次使用,这个线程保证了新提交的页面都是干净的零页面进程虚拟地址空间的布局windows为每个进程提供了平坦的4GB的线性地址空间,这个地址空间被分为用户分区和内核分区,他们各占2GB大小,其中内核分区在高地址位,用户分区在低地址位,下面是内存分布的一个表格:分区地址范围NULL指针区0x00000000-0x0000FFFF用户分区0x00010000-0x7FFEFFFF64K禁入区0x7FFF0000-0x7FFFFFFF内核分区0x80000000-0xFFFFFFFF从上面的图中可以看出,系统的内核分区是2GB而用户可用的分区并没有2GB,在用户分区的头64K和尾部的64K不允许用户使用。另外我们可以压缩内核分区的大小,以便使用户分区占更多的内存,这就是/3GB方式,下面是这种方式的具体内存分布:分区地址范围NULL指针区0x00000000-0x0000FFFF用户分区0x00010000-0xBFFEFFFF64K禁入区0xBFFF0000-0xBFFFFFFF内核分区0xC0000000-0xFFFFFFFFwindows虚拟内存管理函数VirtualAllocVirtualAlloc函数主要用于提交或者保留一段虚拟地址空间,通过该函数提交的页面是经过0页面线程清理的干净的页面。LPVOID VirtualAlloc( LPVOID lpAddress, //虚拟内存的地址 DWORD dwSize, //虚拟内存大小 DWORD flAllocationType,//要对这块的虚拟内存做何种操作 DWORD flProtect //虚拟内存的保护属性 ); 我们可以指定第一个参数来告知系统,我们希望操作哪块内存,如果这个地址对应的内存已经被保留了那么将向下偏移至64K的整数倍,如果这块内存已经被提交,那么地址将向下偏移至4K的整数倍,也就是说保留页面的最小粒度是64K,而提交的最小粒度是一页4K。第三个参数是指定分配的类型,主要有以下几个值值含义MEM_COMMIT提交,也就是说将虚拟地址映射到对应的真实物理内存中,这样这块内存就可以正常使用MEM_RESERVE保留,告知系统以这个地址开始到后面的dwSize大小的连续的虚拟内存程序要使用,进程其他分配内存的操作不得使用这段内存。MEM_TOP_DOWN从高端地址保留空间(默认是从低端向高端搜索)MEM_LARGE_PAGES开启大页面的支持,默认一个页面是4K而大页面是2M(这个视具体系统而定)MEM_WRITE_WATCH开启页面写入监视,利用GetWriteWatch可以得到写入页面的统计情况,利用ResetWriteWatch可以重置起始计数MEM_PHYSICAL用于开启PAE第四个参数主要是页面的保护属性,参数可取值如下:值含义PAGE_READONLY只读PAGE_READWRITE可读写PAGE_EXECUTE可执行PAGE_EXECUTE_READ可读可执行PAGE_EXECUTE_READWRITE可读可写可执行PAGE_NOACCESS不可访问PAGE_GUARD将该页设置为保护页,如果试图对该页面进行读写操作,会产生一个STATUS_GUARD_PAGE 异常下面是该函数使用的几个例子:页面的提交/保留与释放//保留并提交 LPVOID pMem = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); srand((unsigned int)time(NULL)); float* pfMem = (float*)pMem; for (int i = 0; i < 4 * 4096 / sizeof(float); i++) { pfMem[i] = rand(); } //释放 VirtualFree(pMem, 4 * 4096, MEM_RELEASE); //先保留再提交 LPBYTE pByte = (LPBYTE)VirtualAlloc(NULL, 1024 * 1024, MEM_RESERVE, PAGE_READWRITE); VirtualAlloc(pByte + 4 * 4096, 4096, MEM_COMMIT, PAGE_READWRITE); pfMem = (float*)(pByte + 4 * 4096); for (int i = 0; i < 4096/sizeof(float); i++) { pfMem[i] = rand(); } //释放 VirtualFree(pByte + 4 * 4096, 4096, MEM_DECOMMIT); VirtualFree(pByte, 1024 * 1024, MEM_RELEASE);大页面支持//获得大页面的尺寸 DWORD dwLargePageSize = GetLargePageMinimum(); LPVOID pBuffer = VirtualAlloc(NULL, 64 * dwLargePageSize, MEM_RESERVE, PAGE_READWRITE); //提交大页面 VirtualAlloc(pBuffer, 4 * dwLargePageSize, MEM_COMMIT | MEM_LARGE_PAGES, PAGE_READWRITE); VirtualFree(pBuffer, 4 * dwLargePageSize, MEM_DECOMMIT); VirtualFree(pBuffer, 64 * dwLargePageSize, MEM_RELEASE);VirtualProtectVirtualProtect用来设置页面的保护属性,函数原型如下:BOOL VirtualProtect( LPVOID lpAddress, //虚拟内存地址 DWORD dwSize, //大小 DWORD flNewProtect, //保护属性 PDWORD lpflOldProtect //返回原来的保护属性 ); 这个保护属性与之前介绍的VirtualAlloc中的保护属性相同,另外需要注意的一点是一般返回原来的属性的话,这个指针可以为NULL,但是这个函数不同,如果第四个参数为NULL,那么函数调用将会失败LPVOID pBuffer = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); float *pfArray = (float*)pBuffer; for (int i = 0; i < 4 * 4096 / sizeof(float); i++) { pfArray[i] = 1.0f * rand(); } //将页面改为只读属性 DWORD dwOldProtect = 0; VirtualProtect(pBuffer, 4 * 4096, PAGE_READONLY, &dwOldProtect); //写入数据将发生异常 pfArray[9] = 0.1f; VirtualFree(pBuffer, 4 * 4096, MEM_RELEASE);VirtualQuery这个函数用来查询某段虚拟内存的属性信息,这个函数原型如下:DWORD VirtualQuery( LPCVOID lpAddress,//地址 PMEMORY_BASIC_INFORMATION lpBuffer, //用于接收返回信息的指针 DWORD dwLength //缓冲区大小,上述结构的大小 ); 结构MEMORY_BASIC_INFORMATION的定义如下:typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; //该页面的起始地址 PVOID AllocationBase;//分配给该页面的首地址 DWORD AllocationProtect;//页面的保护属性 DWORD RegionSize; //页面大小 DWORD State;//页面状态 DWORD Protect;//页面的保护类型 DWORD Type;//页面类型 } MEMORY_BASIC_INFORMATION; typedef MEMORY_BASIC_INFORMATION *PMEMORY_BASIC_INFORMATION; AllocationProtect与Protect所能取的值与之前的保护属性的值相同。State的取值如下:MEM_FREE:空闲MEM_RESERVE:保留MEM_COMMIT:已提交Type的取值如下:MEM_IMAGE:映射类型,一般是映射到地址控件的可执行模块如DLL,EXE等MEM_MAPPED:文件映射类型MEM_PRIVATE:私有类型,这个页面的数据为本进程私有数据,不能与其他进程共享下面是这个的使用例子:#include<windows.h> #include <stdio.h> #include <tchar.h> #include <atlstr.h> CString GetMemoryInfo(MEMORY_BASIC_INFORMATION *pmi); int _tmain(int argc, TCHAR *argv[]) { SYSTEM_INFO sm = {0}; GetSystemInfo(&sm); LPVOID dwMinAddress = sm.lpMinimumApplicationAddress; LPVOID dwMaxAddress = sm.lpMaximumApplicationAddress; MEMORY_BASIC_INFORMATION mbi = {0}; _putts(_T("BaseAddress\tAllocationBase\tAllocationProtect\tRegionSize\tState\tProtect\tType\n")); for (LPVOID pAddress = dwMinAddress; pAddress <= dwMaxAddress;) { if (VirtualQuery(pAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION)) == 0) { break; } _putts(GetMemoryInfo(&mbi)); //一般通过BaseAddress(页面基地址) + RegionSize(页面长度)来寻址到下一个页面的的位置 pAddress = (BYTE*)mbi.BaseAddress + mbi.RegionSize; } } CString GetMemoryInfo(MEMORY_BASIC_INFORMATION *pmi) { CString lpMemoryInfo = _T(""); int iBaseAddress = (int)(pmi->BaseAddress); int iAllocationBase = (int)(pmi->AllocationBase); CString szProtected = _T("\0"); if (pmi->Protect & PAGE_READONLY) { szProtected = _T("R"); }else if (pmi->Protect & PAGE_READWRITE) { szProtected = _T("RW"); }else if (pmi->Protect & PAGE_WRITECOPY) { szProtected = _T("WC"); }else if (pmi->Protect & PAGE_EXECUTE) { szProtected = _T("X"); }else if (pmi->Protect & PAGE_EXECUTE_READ) { szProtected = _T("RX"); }else if (pmi->Protect & PAGE_EXECUTE_READWRITE) { szProtected = _T("RWX"); }else if (pmi->Protect & PAGE_EXECUTE_WRITECOPY) { szProtected = _T("WCX"); }else if (pmi->Protect & PAGE_GUARD) { szProtected = _T("GUARD"); }else if (pmi->Protect & PAGE_NOACCESS) { szProtected = _T("NOACCESS"); }else if (pmi->Protect & PAGE_NOCACHE) { szProtected = _T("NOCACHE"); }else { szProtected = _T(" "); } CString szAllocationProtect = _T("\0"); if (pmi->AllocationProtect & PAGE_READONLY) { szProtected = _T("R"); }else if (pmi->AllocationProtect & PAGE_READWRITE) { szProtected = _T("RW"); }else if (pmi->AllocationProtect & PAGE_WRITECOPY) { szProtected = _T("WC"); }else if (pmi->AllocationProtect & PAGE_EXECUTE) { szProtected = _T("X"); }else if (pmi->AllocationProtect & PAGE_EXECUTE_READ) { szProtected = _T("RX"); }else if (pmi->AllocationProtect & PAGE_EXECUTE_READWRITE) { szProtected = _T("RWX"); }else if (pmi->AllocationProtect & PAGE_EXECUTE_WRITECOPY) { szProtected = _T("WCX"); }else if (pmi->AllocationProtect & PAGE_GUARD) { szProtected = _T("GUARD"); }else if (pmi->AllocationProtect & PAGE_NOACCESS) { szProtected = _T("NOACCESS"); }else if (pmi->AllocationProtect & PAGE_NOCACHE) { szProtected = _T("NOCACHE"); }else { szProtected = _T(" "); } DWORD dwRegionSize = pmi->RegionSize; CString strState = _T(""); if (pmi->State & MEM_FREE) { strState = _T("Free"); }else if (pmi->State & MEM_RESERVE) { strState = _T("Reserve"); }else if (pmi->State & MEM_COMMIT) { strState = _T("Commit"); }else { strState = _T(" "); } CString strType = _T(""); if (pmi->Type & MEM_IMAGE) { strType = _T("Image"); }else if (pmi->Type & MEM_MAPPED) { strType = _T("Mapped"); }else if (pmi->Type & MEM_PRIVATE) { strType = _T("Private"); } lpMemoryInfo.Format(_T("%08X %08X %s %d %s %s %s\n"), iBaseAddress, iAllocationBase, szAllocationProtect, dwRegionSize, strState, szProtected, strType); return lpMemoryInfo; }VirtualLock和VirtualUnlock这两个函数用于锁定和解锁页面,前面说过操作系统会将长时间不用的内存中的数据放入到系统的磁盘文件中,需要的时候再放回到内存中,这样来回倒腾,必定会造成程序效率的底下,为了避免这中效率底下的操作,可以使用VirtualLock将页面锁定在内存中,防止页面交换,但是不用了的时候需要使用VirtualUnlock来解锁,不然一直锁定而不解锁会造成真实内存的不足。另外需要注意的是,不能一次操作超过工作集规定的最大虚拟内存,这样会造成程序崩溃,我们可以通过函数SetProcessWorkingSetSize来设置工作集规定的最大虚拟内存的大小。下面是一个使用例子:SetProcessWorkingSetSize(GetCurrentProcess(), 1024 * 1024, 2 * 1024 * 1024); LPVOID pBuffer = VirtualAlloc(NULL, 4 * 4096, MEM_RESERVE, PAGE_READWRITE); //不能锁定超过进程工作集大小的虚拟内存 VirtualLock(pBuffer, 3 * 1024 * 1024); //不能一次提交超过进程工作集大小的虚拟内存 VirtualAlloc(pBuffer, 3 * 1024 * 1024, MEM_COMMIT, PAGE_READWRITE); float *pfArray = (float*)pBuffer; for (int i = 0; i < 4096 / sizeof(float); i++) { pfArray[i] = 1.0f * rand(); } VirtualUnlock(pBuffer, 4096); VirtualFree(pBuffer, 4096, MEM_DECOMMIT); VirtualFree(pBuffer, 4 * 4096, MEM_RELEASE);VirtualFreeVirtualFree用于释放申请的虚拟内存。这个函数支持反提交和释放,这两个操作由第三个参数指定:MEM_DECOMMIT:反提交,这样这个线性地址就不再映射到具体的物理内存,但是这个地址仍然是保留地址。MEM_RELEASE:释放,这个范围的地址不再作为保留地址
2016年07月21日
14 阅读
0 评论
0 点赞
2016-04-25
C语言中不同变量的访问方式
C语言中的变量大致可以分为全局变量,局部变量,堆变量和静态局部变量,这些不同的变量存储在不同的位置,有不同的生命周期。一般程序将内存分为数据段、代码段、栈段、堆段,这几类变量存储在不同的段中,造成了它们有不同的生命周期。全局变量全局变量的生命周期是整个程序的生命周期,随着程序的运行而存在,随着程序的结束而消亡,全局变量位于程序的数据段。每个应用程序有4GB的虚拟地址空间,在程序开始时系统将这个程序加载到内存中,为其分配内存,这个时候,会根据程序文件的内容,为全局变量分配内存,并为之进行初始化,当程序的生命周期结束时,系统回收进程所消耗的资源,这个时候,全局变量所占的内存被销毁。下面来看一段具体的代码:int i= 0; int main(int argc, char* argv[]) { printf("%d\n", i); return 0; }11: printf("%d\n", i); 00401268 mov eax,[i (00432e24)] 0040126D push eax 0040126E push offset string "%d\n" (0042e01c)从上述的汇编代码中可以看到,i所对应的地址为0x00432e24,在调用全局变量时,使用的是一个具体的地址,但是并没有看对应初始化i变量的反汇编代码,这是因为在程序开始运行之前,在准备进程环境的时候就为i分配的了存储空间,并进行了初始化。另外在使用时采用的是直接寻址的方式,并没有用寄存器来进行间接寻址,从这点上来看,i变量的地址不会随着程序的运行而改变,这个地址一直可以使用,所以全局变量的生命周期与程序的生命周期相同。静态变量静态变量有两个作用,一是将变量名所能使用的区域限定在对应位置,比如我们在一个函数中定义了一个静态变量,那么久只能在这个函数中使用这个变量,二是静态变量的生命周期是全局的,不会随着堆栈环境的改变而改变,下面是一个简单的例子int Func() { static int i = 0; i++; return i; } int main() { printf("%d\n", Func()); printf("%d\n", Func()); return 0; }9: static int i = 0; 10: i++; 00401268 mov eax,[_Ios_init+3 (00433e24)] 0040126D add eax,1 00401270 mov [_Ios_init+3 (00433e24)],eax 11: return i; 上面的汇编代码也采用的是直接寻址的方式,而这个静态变量的地址为0x433e24,与上面的全局变量的地址进行比较,我们可以看出,其实它也是在全局作用域的,在初始化时也没有发现有任何的初始化代码,所以我们可以说,它的生命周期也是全局的,但是由于static将其可见域限定在函数中,所以在函数外不能通过这个变量名来访问这块内存区域。局部静态变量的工作方式上面说到局部静态变量的生命周期不随函数的结束而结束,不管进入函数多少次,局部静态变量只有一个内存地址,而且只初始化一次,具体编译器是如何做到的,将用下面这一段代码来说明:int test(int n) { static int i = n; return i; } int main(int argc, char* argv[]) { for (int i = 0; i < 5; i++) { printf("%d\n", test(i)); } return 0; }12: static int i = n; 00401268 xor eax,eax 0040126A mov al,[`test'::`2'::$S25 (00433e24)];用一个字节存储了一个标志位 0040126F and eax,1 00401272 test eax,eax 00401274 jne test+3Eh (0040128e);当该标志位为1则表明进行了初始化,直接跳过初始化的步骤 00401276 mov cl,byte ptr [`test'::`2'::$S25 (00433e24)] 0040127C or cl,1;没有进行初始化的话,先初始化然后将标志位赋值为1 0040127F mov byte ptr [`test'::`2'::$S25 (00433e24)],cl 00401285 mov edx,dword ptr [ebp+8] 00401288 mov dword ptr [__pInconsistency+39Ch (00433e20)],edx 13: return i; 0040128E mov eax,[__pInconsistency+39Ch (00433e20)]在上面这段代码中我们企图多次对静态变量进行初始化,但是通过运行程序最终得到的结果都是一样的,上述的代码并没有改变静态变量的值,通过查看汇编代码我们可以看到,编译器在处理局部静态变量时多用了一个字节的内存保存了一个标志位,当该静态变量进行了初始化的时候,就跳过初始化的代码,否则进行初始化并将标志位赋相应的值。局部变量局部变量,的生命周期随着函数的调用而存在,当函数结束时它的生命周期就结束了。在我的上一篇将函数的博客中,已经说明了它寻址方式和生命周期。在函数调用时,会首先根据函数中局部变量所占的空间,初始化栈环境,并对这些局部变量进行初始化,当函数调用完成后,会首先回收栈环境,这样局部变量所在的内存被回收,用于下一个函数调用或者用作其他用途,因为栈是动态变化的,为了防止使用不当造成程序错误,所以在函数外是不能使用函数中定义的局部变量。另外一个需要说明的就是在语句块内的局部变量,它的生命周期只在语句块中,但是真实的情况是,它所在的内存与局部变量相同,都是在函数栈中,它的生命周期只在语法层面上进行限制。堆变量堆变量需要程序员自己申请并释放,需要程序员自己管理,程序不会自动管理这些内存,当调用malloc或者new 的时候,系统分配一块内存,直到调用free 或者delete的时候才释放。
2016年04月25日
22 阅读
0 评论
0 点赞
2016-04-10
IF和SWITCH的原理
在C语言中,if和switch是条件分支的重要组成部分。ifif的功能是计算判断条件的值,根据返回的值的不同来决定跳转到哪个部分。值为真则跳转到if语句块中,否则跳过if语句块。下面来分析一个简单的if实例:if(argc > 0) { printf("argc > 0\n"); } if (argc <= 0) { printf("argc <= 0\n"); } printf("argc = %d\n", argc);它对应的汇编代码如下:9: if(argc > 0) cmp dword ptr [ebp+8],0 0040102C jle main+2Bh (0040103b) ;argc <= 0就跳转到下一个if处 10: { 11: printf("argc > 0\n"); 0040102E push offset string "argc > 0\n" (0042003c) call printf (00401090) add esp,4 12: } 13: if (argc <= 0) ;argc > 0跳转到后面的printf语句输出argc的值 0040103B cmp dword ptr [ebp+8],0 0040103F jg main+3Eh (0040104e) 14: { 15: printf("argc <= 0\n"); push offset string "argc <= 0\n" (0042002c) call printf (00401090) 0040104B add esp,4 16: } 17: printf("argc = %d\n", argc); 0040104E mov eax,dword ptr [ebp+8] push eax push offset string "argc = %d\n" (0042001c) call printf (00401090) 0040105C add esp,8根据汇编代码我们看到,首先执行第一个if中的比较,jle表示当cmp得到的结果≤0时会进行跳转,第二个if在汇编中的跳转条件是>0,从这个上面可以看出在代码执行过程当中if转换的条件判断语句与if的判断结果时相反的,也就是说cmp比较后不成立则跳转,成立则向下执行。同时每一次跳转都是到当前if语句的下一条语句。if ...else下面来看看if...else...语句的跳转。if(argc > 0) { printf("argc > 0\n"); }else { printf("argc <= 0\n"); } printf("argc = %d\n", argc);它所对应的汇编代码如下:00401028 cmp dword ptr [ebp+8],0 0040102C jle main+2Dh (0040103d) ;条件不满足则跳转到else语句块中 10: { 11: printf("argc > 0\n"); 0040102E push offset string "argc > 0\n" (0042003c) 00401033 call printf (00401090) 00401038 add esp,4 12: }else 0040103B jmp main+3Ah (0040104a);如果执行if语句块就会执行这条语句跳出else语句块 13: { 14: printf("argc <= 0\n"); 0040103D push offset string "argc <= 0\n" (0042002c) 00401042 call printf (00401090) 00401047 add esp,4 15: } 16: printf("argc = %d\n", argc); 0040104A mov eax,dword ptr [ebp+8]上述的汇编代码指出,对于if...else..语句,首先进行条件判断,if表达式为真,则继续执行if快中的语句,然后利用jmp跳转到else语句块外,否则会利用jmp跳转到else语句块中,然后依次执行其后的每一句代码。if ... else if... else最后再来展示if...else if...else这种分支结构:if(argc > 0) { printf("argc > 0\n"); }else if(argc < 0) { printf("argc < 0\n"); }else { printf("argc == 0\n"); } printf("argc = %d\n", argc);汇编代码如下:9: if(argc > 0) 00401028 cmp dword ptr [ebp+8],0 0040102C jle main+2Dh (0040103d);条件不满足则会跳转到下一句else if中 10: { 11: printf("argc > 0\n"); 0040102E push offset string "argc > 0\n" (00420f9c) 00401033 call printf (00401090) 00401038 add esp,4 12: }else if(argc < 0) 0040103B jmp main+4Fh (0040105f) ;当上述条件符合则执行这条语句跳出分支外,跳转的地址正是else语句外的printf语句 0040103D cmp dword ptr [ebp+8],0 00401041 jge main+42h (00401052) 13: { 14: printf("argc < 0\n"); 00401043 push offset string "argc < 0\n" (0042003c) 00401048 call printf (00401090) 0040104D add esp,4 15: }else 00401050 jmp main+4Fh (0040105f) 16: { 17: printf("argc == 0\n"); 00401052 push offset string "argc <= 0\n" (0042002c) 00401057 call printf (00401090) 0040105C add esp,4 18: } 19: printf("argc = %d\n", argc); 0040105F mov eax,dword ptr [ebp+8]通过汇编代码可以看到对于这种结构,会依次判断每个if语句中的条件,当有一个满足,执行完对应语句块中的代码后,会直接调转到分支结构外部,当前面的条件都不满足则会执行else语句块中的内容。这个逻辑结构在某些情况下可以利用if return if return 这种结构来替代。当某一条件满足时执行完对应的语句后直接返回而不执行其后的代码。一条提升效率的做法是将最有可能满足的条件放在前面进行比较,这样可以减少比较次数,提升效率。switchswitch是另一种比较常用的多分支结构,在使用上比较简单,效率上也比if...else if...else高,下面将分析switch结构的实现switch(argc) { case 1: printf("argc = 1\n"); break; case 2: printf("argc = 2\n"); break; case 3: printf("argc = 3\n"); break; case 4: printf("argc = 4\n"); break; case 5: printf("argc = 5\n"); break; case 6: printf("argc = 6\n"); break; default: printf("else\n"); break; }对应的汇编代码如下:0040B798 mov eax,dword ptr [ebp+8] ;eax = argc 0040B79B mov dword ptr [ebp-4],eax 0040B79E mov ecx,dword ptr [ebp-4] ;ecx = eax 0040B7A1 sub ecx,1 0040B7A4 mov dword ptr [ebp-4],ecx 0040B7A7 cmp dword ptr [ebp-4],5 0040B7AB ja $L544+0Fh (0040b811) ;argc 》 5则跳转到default处,至于为什么是5而不是6,看后面的说明 0040B7AD mov edx,dword ptr [ebp-4] ;edx = argc 0040B7B0 jmp dword ptr [edx*4+40B831h] 11: case 1: 12: printf("argc = 1\n"); 0040B7B7 push offset string "argc = 1\n" (00420fc0) 0040B7BC call printf (00401090) 0040B7C1 add esp,4 13: break; 0040B7C4 jmp $L544+1Ch (0040b81e) 14: case 2: 15: printf("argc = 2\n"); 0040B7C6 push offset string "argc = 2\n" (00420fb4) 0040B7CB call printf (00401090) 0040B7D0 add esp,4 16: break; 0040B7D3 jmp $L544+1Ch (0040b81e) 17: case 3: 18: printf("argc = 3\n"); 0040B7D5 push offset string "argc = 3\n" (00420fa8) 0040B7DA call printf (00401090) 0040B7DF add esp,4 19: break; 0040B7E2 jmp $L544+1Ch (0040b81e) 20: case 4: 21: printf("argc = 4\n"); 0040B7E4 push offset string "argc = 4\n" (00420f9c) 0040B7E9 call printf (00401090) 0040B7EE add esp,4 22: break; 0040B7F1 jmp $L544+1Ch (0040b81e) 23: case 5: 24: printf("argc = 5\n"); 0040B7F3 push offset string "argc < 0\n" (0042003c) 0040B7F8 call printf (00401090) 0040B7FD add esp,4 25: break; 0040B800 jmp $L544+1Ch (0040b81e) 26: case 6: 27: printf("argc = 6\n"); 0040B802 push offset string "argc <= 0\n" (0042002c) 0040B807 call printf (00401090) 0040B80C add esp,4 28: break; 0040B80F jmp $L544+1Ch (0040b81e) 29: default: 30: printf("else\n"); 0040B811 push offset string "argc = %d\n" (0042001c) 0040B816 call printf (00401090) 0040B81B add esp,4 31: break; 32: } 33: 34: return 0; 0040B81E xor eax,eax上面的代码中并没有看到像if那样,对每一个条件都进行比较,其中有一句话 “jmp dword ptr [edx*4+40B831h]” 这句话从表面上看应该是取数组中的元素,再根据元素的值来进行跳转,而这个元素在数组中的位置与eax也就是与argc的值有关,下面我们跟踪到数组中查看数组的元素值:0040B831 B7 B7 40 00 0040B835 C6 B7 40 00 0040B839 D5 B7 40 00 0040B83D E4 B7 40 00 0040B841 F3 B7 40 00 0040B845 02 B8 40 00通过对比可以发现0x0040b7b7是case 1处的地址,后面的分别是case 2、case 3、case 4、case 5、case 6处的地址,每个case中的break语句都翻译为了同一句话“jmp $L544+1Ch (0040b81e)”,所以从这可以看出,在switch中,编译器多增加了一个数组用于存储每个case对应的地址,根据switch中传入的整数在数组中查到到对应的地址,直接通过这个地址跳转到对应的位置,减少了比较操作,提升了效率。编译器在处理switch时会首先校验不满足所有case的情况,当这种情况发生时代码调转到default或者switch语句块之外。然后将传入的整数值减一(数组元素是从0开始计数)。最后根据参数值找到应该跳转的位置。上述的代码case是从0~6依次递增,这样做确实可行,但是当我们在case中的值并不是依次递增的话会怎样?此时根据不同的情况编译器会做不同的处理。一般任然会建立这样的一个表,将case中出现的值填写对应的跳转地址,没有出现的则将这个地址值填入default对应的地址或者switch语句结束的地址,比如当我们上述的代码去掉case 5, 这个时候填入的地址值如下图所示:如果每两个case之间的差距大于6,或者case语句数小于4则不会采取这种做法,如果再采用这种方式,那么会造成较大的资源消耗。这个时候编译器会采用索引表的方式来进行地址的跳转。下面有这样一个例子:switch(argc) { case 1: printf("argc = 1\n"); break; case 2: printf("argc = 2\n"); break; case 5: printf("argc = 5\n"); break; case 6: printf("argc = 6\n"); break; case 255: printf("argc = 255\n"); default: printf("else\n"); break; }它对应的汇编代码如下:0040B798 mov eax,dword ptr [ebp+8] 0040B79B mov dword ptr [ebp-4],eax 0040B79E mov ecx,dword ptr [ebp-4] ;到此eax = ecx = argc 0040B7A1 sub ecx,1 0040B7A4 mov dword ptr [ebp-4],ecx 0040B7A7 cmp dword ptr [ebp-4],0FEh 0040B7AE ja $L542+0Dh (0040b80b) ;当argc > 255则跳转到default处 0040B7B0 mov eax,dword ptr [ebp-4] 0040B7B3 xor edx,edx 0040B7B5 mov dl,byte ptr (0040b843)[eax] 0040B7BB jmp dword ptr [edx*4+40B82Bh] 11: case 1: 12: printf("argc = 1\n"); 0040B7C2 push offset string "argc = 1\n" (00420fb4) 0040B7C7 call printf (00401090) 0040B7CC add esp,4 13: break; 0040B7CF jmp $L542+1Ah (0040b818) 14: case 2: 15: printf("argc = 2\n"); 0040B7D1 push offset string "argc = 3\n" (00420fa8) 0040B7D6 call printf (00401090) 0040B7DB add esp,4 16: break; 0040B7DE jmp $L542+1Ah (0040b818) 17: case 5: 18: printf("argc = 5\n"); 0040B7E0 push offset string "argc = 5\n" (00420f9c) 0040B7E5 call printf (00401090) 0040B7EA add esp,4 19: break; 0040B7ED jmp $L542+1Ah (0040b818) 20: case 6: 21: printf("argc = 6\n"); 0040B7EF push offset string "argc < 0\n" (0042003c) 0040B7F4 call printf (00401090) 0040B7F9 add esp,4 22: break; 0040B7FC jmp $L542+1Ah (0040b818) 23: case 255: 24: printf("argc = 255\n"); 0040B7FE push offset string "argc <= 0\n" (0042002c) 0040B803 call printf (00401090) 0040B808 add esp,4 25: default: 26: printf("else\n"); 0040B80B push offset string "argc = %d\n" (0042001c) 0040B810 call printf (00401090) 0040B815 add esp,4 27: break; 28: } 29: 30: return 0; 0040B818 xor eax,eax这段代码与上述的线性表相比较区别并不大,只是多了一句 “mov dl,byte ptr (0040b843)[eax]” 这似乎又是一个数组,通过查看内存可以知道这个数组的值分别为:00 01 05 05 02 03 05 05 ... 04,下一句根据这些值在另外一个数组中查找数据,我们列出另外一个数组的值:C2 B7 40 00 D1 B7 40 00 E0 B7 40 00 EF B7 40 00 FE B7 40 00 0B B8 40 00通过对比我们发现,这些值分别是每个case与default入口处的地址,编译器先查找到每个值在数组中对应的元素位置,然后根据这个位置值再在地址表中从、找到地址进行跳转,这个过程可以用下面的图来表示:这样通过一个每个元素占一个字节的表,来表示对应的case在地址表中所对应的位置,从而跳转到对应的地址,这样通过对每个case增加一个字节的内存消耗来达到,减少地址表对应的内存消耗。在上述的汇编代码中,是利用dl寄存器来存储对应case在地址表中项,这样就会产生一个问题,当case 值大于 255,也就是超出了一个字节的,超出了dl寄存器的表示范围时,又该如何来进行跳转这个时候编译器会采用判定树的方式来进行判定,在根节点保存的是所有case值的中位数, 左子树都是大于这个大于这个值的数,右字数是小于这个值的数,通过每次的比较来得到正确的地址。比如下面的这个判定树:首先与10进行比较,根据与10 的大小关系进入左子树或者右子树,再看看左右子树的分支是否不大于3,若不大于3则直接转化为对应的if...else if... else结构,大于3则检测分支是否满足上述的优化条件,满足则进行对应的地址表或者索引表的优化,否则会再次对子树进行优化,以便减少比较次数。
2016年04月10日
19 阅读
0 评论
0 点赞
2016-01-03
地址、指针与引用
计算机本身是不认识程序中给的变量名,不管我们以何种方式给变量命名,最终都会转化为相应的地址,编译器会生成一些符号常量并且与对应的地址相关联,以达到访问变量的目的。 变量是在内存中用来存储数据以供程序使用,变量主要有两个部分构成:变量名、变量类型,其中变量名对应了一块具体的内存地址,而变量类型则表明该如何翻译内存中存储的二级制数。我们知道不同的类型翻译为二进制的值不同,比如整型是直接通过数学转化、浮点数是采用IEEE的方法、字符则根据ASCII码转化,同样变量类型决定了变量所占的内存大小,以及如何在二进制和变量所表达的真正意义之间转化。而指针变量也是一个变量,在内存中也占空间,不过比较特殊的是它存储的是其他变量的地址。在32位的机器中,每个进程能访问4GB的内存地址空间,所以程序中的地址采用32位二进制数表示,也就是一个整型变量的长度,地址值一般没有负数所以准确的说指针变量的类型应该是unsigned int 即每个指针变量占4个字节。还记得在定义结构体中可以使用该结构体的指针作为成员,但是不能使用该结构的实例作为成员吗?这是因为编译器需要根据各个成员变量的大小分配相关的内存,用该结构体的实例作为成员时,该结构体根本没有定义完整,编译器是不会知道该如何分配内存的,而任何类型的指针都只占4个字节,编译器自然知道如何分配内存。我们在书写指针变量时给定的类型是它所指向的变量的类型,这个类型决定了如何翻译所对应内存中的值,以及该访问多少个字节的内存。对指针的间接访问会先先取出值,访问到对应的内存,再根据指针所指向的变量的类型,翻译成对应的值。一般指针只能指向对应类型的变量,比如int类型的指针只能指向int型的变量,而有一种指针变量可以指向所有类型的变量,它就是void类型的指针变量,但是由于这种类型的变量没有指定它所对应的变量的类型,所以即使有了对应的地址,它也不知道该取多大内存的数据,以及如何解释这些数据,所以这种类型的指针不支持间接访问,下面是一个间接访问的例子:int main() { int nValue = 10; float fValue = 10.0f; char cValue = 'C'; int *pnValue = &nValue; float *pfValue = &fValue; char *pcValue = &cValue; printf("pnValue = %x, *pnValue = %d\n", pnValue, *pnValue); printf("pfValue = %x, *pfValue = %f\n", pfValue, *pfValue); printf("pcValue = %x, *pcValue = %c\n", pcValue, *pcValue); return 0; }下面是它对应的反汇编代码(部分):10: int nValue = 10; 00401268 mov dword ptr [ebp-4],0Ah 11: float fValue = 10.0f; 0040126F mov dword ptr [ebp-8],41200000h 12: char cValue = 'C'; 00401276 mov byte ptr [ebp-0Ch],43h 13: int *pnValue = &nValue; 0040127A lea eax,[ebp-4] 0040127D mov dword ptr [ebp-10h],eax 14: float *pfValue = &fValue; 00401280 lea ecx,[ebp-8] 00401283 mov dword ptr [ebp-14h],ecx 15: char *pcValue = &cValue; 00401286 lea edx,[ebp-0Ch] 00401289 mov dword ptr [ebp-18h],edx 16: printf("pnValue = %x, *pnValue = %d\n", pnValue, *pnValue); 0040128C mov eax,dword ptr [ebp-10h] 0040128F mov ecx,dword ptr [eax] 00401291 push ecx 00401292 mov edx,dword ptr [ebp-10h] 00401295 push edx 00401296 push offset string "pnValue = %x, *pnValue = %d\n" (00432064) 0040129B call printf (00401580) 004012A0 add esp,0Ch从上面的汇编代码可以看到指针变量会占内存空间,它们的地址分别是:[ebp - 10h] 、 [ebp - 14h]、 [ebp - 18h],在给指针变量赋值时首先将变量的地址赋值给临时寄存器,然后将寄存器的值赋值给指针变量,而通过间接访问时也经过了一个临时寄存器,先将指针变量的值赋值给临时寄存器(mov eax,dword ptr [ebp-10h])然后通过这个临时寄存器访问变量的地址空间,得到变量值( mov ecx,dword ptr [eax]),由于间接访问进过了这几步,所以在效率上是比不上直接使用变量。下面是对char型变量的间接访问:004012BF mov edx,dword ptr [ebp-18h] 004012C2 movsx eax,byte ptr [edx] 004012C5 push eax首先也是将指针变量的值取出来,放到寄存器中,然后根据寄存器寻址找到变量对应的地址,访问变量。其中”bye ptr“表示只操作该地址中的一个字节。对于地址我们可以进行加法和减法操作,地址的加法主要用于向下寻址,一般用于数组等占用连续内存空间的数据结构,一般是地址加上一个数值,表示向后偏移一定的单位,指针同样也有这样的操作,但是与地址值不同的是指针每加一个单位,表示向后偏移一个元素,而地址值加1则就是在原来的基础上加上一。指针偏移是根据其所指向的变量类型来决定的,比如有下面的程序:int main(int argc, char* argv[]) { char szBuf[5] = {0x01, 0x23, 0x45, 0x67, 0x89}; int *pInt = (int*)szBuf; short *pShort = (short*)szBuf; char *pChar = szBuf; pInt += 1; pShort += 1; pChar += 1; return 0; }它的汇编代码如下:9: char szBuf[5] = {0x01, 0x23, 0x45, 0x67, 0x89}; 00401028 mov byte ptr [ebp-8],1 0040102C mov byte ptr [ebp-7],23h 00401030 mov byte ptr [ebp-6],45h 00401034 mov byte ptr [ebp-5],67h 00401038 mov byte ptr [ebp-4],89h 10: int *pInt = (int*)szBuf; 0040103C lea eax,[ebp-8] 0040103F mov dword ptr [ebp-0Ch],eax 11: short *pShort = (short*)szBuf; 00401042 lea ecx,[ebp-8] 00401045 mov dword ptr [ebp-10h],ecx 12: char *pChar = szBuf; 00401048 lea edx,[ebp-8] 0040104B mov dword ptr [ebp-14h],edx 13: 14: pInt += 1; 0040104E mov eax,dword ptr [ebp-0Ch] 00401051 add eax,4 00401054 mov dword ptr [ebp-0Ch],eax 15: pShort += 1; 00401057 mov ecx,dword ptr [ebp-10h] 0040105A add ecx,2 0040105D mov dword ptr [ebp-10h],ecx 16: pChar += 1; 00401060 mov edx,dword ptr [ebp-14h] 00401063 add edx,1 00401066 mov dword ptr [ebp-14h],edx根据其汇编代码可以看出,对于int型的指针,每加1个会向后偏移4个字节,short会偏移2个字节,char型的会偏移1个,所以根据以上的内容,可以得出一个公式:TYPE P p + n = p + sizeof(TYPE) n根据上面的加法公式我们可以推导出两个指针的减法公式,TYPE p1, TYPE p2: p2 - p1 = ((int)p2 - (int)p1) / sizeof(TYPE),两个指针相减得到的结果是两个指针之间拥有元素的个数。只有同类型的指针之间才可以相减。而指针的乘除法则没有意义,地址之间的乘除法也没有意义。引用是在C++中提出的,是变量的一个别名,提出引用主要是希望减少指针的使用,引用于指针在一个函数中想上述例子中那样使用并没有太大的意义,大量使用它们是在函数中,作为参数传递,不仅可以节省效率,同时也可以传递一段缓冲,作为输出参数来使用。这大大提升了程序的效率以及灵活性。但是在一些新手程序员看来指针无疑是噩梦般的存在,所以C++引入了引用,希望代替指针。在一般的C++书中都说引用是变量的一个别名是不占内存的,但是我通过查看反汇编代码发现引用并不是向书上说的那样,下面是一段程序及它的反汇编代码:int nValue = 10; int &rValue = nValue; printf("%d\n", rValue);10: int nValue = 10; 00401268 mov dword ptr [ebp-4],0Ah 11: int &rValue = nValue; 0040126F lea eax,[ebp-4] 00401272 mov dword ptr [ebp-8],eax 12: printf("%d\n", rValue); 00401275 mov ecx,dword ptr [ebp-8] 00401278 mov edx,dword ptr [ecx] 0040127A push edx 0040127B push offset string "%d\n" (0042e01c) 00401280 call printf (00401520)从汇编代码中可以看到,在定义引用并为它赋值的过程中,编译器其实是将变量的地址赋值给了一个新的变量,这个变量的地址是[ebp - 8h],在调用printf函数的时候,编译器将地址取出并将它压到函数栈中。下面是将引用改为指针的情况:10: int nValue = 10; 00401268 mov dword ptr [ebp-4],0Ah 11: int *pValue = &nValue; 0040126F lea eax,[ebp-4] 00401272 mov dword ptr [ebp-8],eax 12: printf("%d\n", *pValue); 00401275 mov ecx,dword ptr [ebp-8] 00401278 mov edx,dword ptr [ecx] 0040127A push edx 0040127B push offset string "%d\n" (0042e01c) 00401280 call printf (00401520)两种情况的汇编代码完全一样,也就是说引用其实就是指针,编译器将其包装了一下,使它的行为变得和使用变量相同,而且在语法层面上做了一个限制,引用在定义的时候必须初始化,且初始化完成后就不能指向其他变量,这个行为与常指针相同。
2016年01月03日
11 阅读
0 评论
0 点赞
2015-12-27
C/C++中整数与浮点数在内存中的表示方式
在C/C++中数字类型主要有整数与浮点数两种类型,在32位机器中整型占4字节,浮点数分为float,double两种类型,其中float占4字节,而double占8字节。下面来说明它们在内存中的具体表现形式:整型整型变量占4字节,在计算机中都是用二进制表示,整型有无符号和有符号两种形式。无符号变量在定义时只需要在相应类型名前加上unsigned 无符号整型变量用32位的二进制数字表示,在与十进制进行转化时只需要知道计算规则即可轻松转化。需要注意的是在计算机中一般使用主机字节序,即采用“高高低低的方式”,数字高位在高地址位,低位在低地址位,例如我们有一个整数0x10203040那么它在内存中存储的格式为:04 03 02 01。有符号数将最高位表示为符号位,0为正数,1为负数其余位都表示具体的数值,对于负数采用的是补码的方式,补码的规则是用0x100000000减去这个数的绝对值,也可以简单的几位将这个数的绝对值取反加1,这样做是为了方便将减法转化为加法,在数学中两个互为相反数的和为0,比如现在有一个负数数x,那么这个x + |x| = 0这个x的绝对值是一个正数,但是用二级制表示的两个数相加不会等于0,而计算机对于溢出采用的是简单的将溢出位丢弃,所以令x + |x| = 0x100000000,这个最高位1,已经溢出,所以这个结果用四字节保存结果肯定会是0,所以最终得到的x = 0x100000000 - |x|。浮点数:早期的小数表示采用的固定小数点的方式,比如规定在32位二级制数字当中,哪几位表示整数部分,其余的表示小数部分,这样表示的数据范围有限,后来采用的是小数点浮动变化的表示方式,也就是所谓的浮点数。浮点数采用的是IEEE的表示方式,最高位表示符号位,在剩余的31位中,从左往右8位表示的是科学计数法的指数部分,其余的表示整数部分。例如我们将12.25f化为浮点数的表示方式:首先将它化为二进制表示1100.01,利用科学计数法可以表述为:1.10001 * 2^3分解出各个部分:指数部分3 + 127= 011 + 0111111、尾数数部分:10001需要注意的是:因为用科学计数法来表示的话,最高位肯定为1所以这个1不会被表示出来指数部分也有正负之分,最高位为1表示正指数,为0表示负指数,所以算出来指数部分后需要加上127进行转化。将这个转化为对应的32位二级制,尾数部分从31位开始填写,不足部分补0即:0 | 10000010 | 10001 |000000000000000000,隔开的位置分别为符号位、指数位,尾数位。因为有的浮点数没有办法完全化为二进制数,会产生一个无限值,编译器会舍弃一部分内容,也就说只能表示一个近似的数,所以在比较浮点数是否为0的时候不要用==而应该用近似表示,允许一定的误差,比如下面的代码:float fTemp = 0.0001f if(fFloat >= -fTemp && fFloat <= fTemp) { //这个是比较fFloat为0 }double类型的浮点数的编码方式与float相同,只是位数不同。double用11位表示指数部分,其余的表示尾数部分。浮点数的计算在CPU中有专门的浮点数寄存器,和对应的计算指令,在效率上比整型数据的低。在写程序的时候,我们利用变量名来进行变量的识别,但是计算机根本不认识这些变量名,计算机中采用的是直接使用地址的方式找到对应的变量,同时为了能准确找到对应的变量,编译器会生成一个结构专门用于保存变量的标识名与对应的地址,这个标识名不是我们定义的变量名,而是在此基础上添加了一些符号,如下面的例子:extern int nTemp; int main() { cout<<nTemp<<endl; }我们申明一个变量,然后在不定义它的情况下,直接使用,这个时候编译器会报错,表示找不到这个变量,报错的截图如下:我们可以看到编译器为这个变量准备的名称并不是我们所定义的nTemp,而是添加了其他标示。在声明变量的时候编译器会为它准备一个标示名称,在定义时会给它一个对应的内存地址,以后在访问这个标示的时候编译器直接去它对应的内存位置去寻找它,下面我们添加这个变量的定义代码:extern int nTemp;int nTemp = 0;int main(){cout<<nTemp<<endl; return 0;}我们查看对应的汇编代码:11: ;int nTemp = 0;00401798 mov dword ptr [ebp-4],012: ;cout<<nTemp<<endl;我们可以看到在为这个变量初始化的时候编译器是直接找到对应的地址[ebp - 4],没有出现相关的变量名,所以说我们定义的变量名只是为了程序员能够识别,而计算机是直接采用寄存器寻址的方式来取用变量。在编译器中同时也看不到与变量类型相关的代码,编译器在使用变量是只关心它的位置,存储的值,以及如何将其中的二进制翻译为对应的内容,代码如下:int main(){int nTemp = 0x00010101; float *pFloat = (float*)&nTemp; char *pChar = (char*)&nTemp; cout<<nTemp<<endl; cout<<*pFloat<<endl; cout<<pChar<<endl; return 0;}结果如下:从这可以看出同一块内存因为编译器根据类型将它翻译为不同的内容,所展现的内容不同。
2015年12月27日
12 阅读
0 评论
0 点赞
1
...
8
9