读书笔记--[计算机病毒解密与对抗]

目录:

  1. 遍历进程&线程程序
  2. 终止进程
  3. 获取进程信息
  4. 获取进程内模块信息
  5. 获取进程命令行参数

代码运行环境:Win7 x64

VS2012 Update3

遍历系统中所有进程

  1. #include <stdio.h>
  2. #include <windows.h>
  3. #include <TlHelp32.h>
  4. int main()
  5. {
  6. // 为进程的所有线程拍个快照
  7. HANDLE hSnapshort = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  8. if( hSnapshort==INVALID_HANDLE_VALUE )
  9. {
  10. printf("CreateToolhelp32Snapshot调用失败!\n");
  11. return -1;
  12. }
  13. // 获得线程列表,里面记录了线程的详细信息,再使用Thread32First和Thread32Next遍历快照中记录的每个线程信息
  14. PROCESSENTRY32 stcProcessInfo;
  15. stcProcessInfo.dwSize = sizeof(stcProcessInfo);
  16. BOOL  bRet = Process32First(hSnapshort, &stcProcessInfo);
  17. printf("进程名\t\t\t 进程ID\t 线程数\t 父进程ID\n");
  18. while (bRet)
  19. {
  20. printf("%ls\t\t %d\t %d\t %d\n", stcProcessInfo.szExeFile, stcProcessInfo.th32ProcessID, stcProcessInfo.cntThreads, stcProcessInfo.th32ParentProcessID);
  21. bRet = Process32Next(hSnapshort, &stcProcessInfo);
  22. }
  23. CloseHandle(hSnapshort);
  24. system("pause");
  25. return 0;
  26. }
#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h> int main()
{
// 为进程的所有线程拍个快照
HANDLE hSnapshort = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if( hSnapshort==INVALID_HANDLE_VALUE )
{
printf("CreateToolhelp32Snapshot调用失败!\n");
return -1;
} // 获得线程列表,里面记录了线程的详细信息,再使用Thread32First和Thread32Next遍历快照中记录的每个线程信息
PROCESSENTRY32 stcProcessInfo;
stcProcessInfo.dwSize = sizeof(stcProcessInfo); BOOL bRet = Process32First(hSnapshort, &stcProcessInfo); printf("进程名\t\t\t 进程ID\t 线程数\t 父进程ID\n"); while (bRet)
{
printf("%ls\t\t %d\t %d\t %d\n", stcProcessInfo.szExeFile, stcProcessInfo.th32ProcessID, stcProcessInfo.cntThreads, stcProcessInfo.th32ParentProcessID); bRet = Process32Next(hSnapshort, &stcProcessInfo);
} CloseHandle(hSnapshort); system("pause");
return 0;
}

遍历系统中所有线程

  1. #include <stdio.h>
  2. #include <windows.h>
  3. #include <TlHelp32.h>
  4. int main()
  5. {
  6. // 为进程的所有线程拍个快照
  7. HANDLE hSnapshort = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
  8. if( hSnapshort==INVALID_HANDLE_VALUE )
  9. {
  10. printf("CreateToolhelp32Snapshot调用失败!\n");
  11. return -1;
  12. }
  13. // 获得线程列表,里面记录了线程的详细信息,再使用Thread32First和Thread32Next遍历快照中记录的每个线程信息
  14. THREADENTRY32 stcThreadInfo;
  15. stcThreadInfo.dwSize = sizeof(stcThreadInfo);
  16. BOOL  bRet = Thread32First(hSnapshort, &stcThreadInfo);
  17. DWORD dwProId = -1;  // 保存上一个线程的进程ID
  18. unsigned unCount=0;
  19. while (bRet)
  20. {
  21. if( dwProId!=stcThreadInfo.th32OwnerProcessID )
  22. {
  23. // 记录PID与所属PID不同,遍历至不属于同一进程的线程
  24. if( dwProId!=-1 )
  25. {
  26. // 不是第一次遍历
  27. printf("\n进程%d的线程总数:%d\n", dwProId, unCount);
  28. unCount = 0;
  29. printf("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n\n");
  30. }
  31. dwProId = stcThreadInfo.th32OwnerProcessID;
  32. printf("进程%d:\n\n ", dwProId);
  33. printf("线程TID\t\t线程所属进程PID\t\t线程优先级\n");
  34. }
  35. printf("  %d\t\t\t%d\t\t\t %d\n",stcThreadInfo.th32ThreadID, stcThreadInfo.th32OwnerProcessID, stcThreadInfo.tpBasePri);
  36. unCount++;
  37. bRet = Thread32Next(hSnapshort, &stcThreadInfo);
  38. }
  39. printf("\n进程%d的线程总数:%d\n", dwProId, unCount);
  40. unCount = 0;
  41. printf("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n\n");
  42. CloseHandle(hSnapshort);
  43. system("pause");
  44. return 0;
  45. }
#include <stdio.h>
#include <windows.h>
#include <TlHelp32.h> int main()
{
// 为进程的所有线程拍个快照
HANDLE hSnapshort = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if( hSnapshort==INVALID_HANDLE_VALUE )
{
printf("CreateToolhelp32Snapshot调用失败!\n");
return -1;
} // 获得线程列表,里面记录了线程的详细信息,再使用Thread32First和Thread32Next遍历快照中记录的每个线程信息
THREADENTRY32 stcThreadInfo;
stcThreadInfo.dwSize = sizeof(stcThreadInfo); BOOL bRet = Thread32First(hSnapshort, &stcThreadInfo);
DWORD dwProId = -1; // 保存上一个线程的进程ID
unsigned unCount=0; while (bRet)
{
if( dwProId!=stcThreadInfo.th32OwnerProcessID )
{ // 记录PID与所属PID不同,遍历至不属于同一进程的线程
if( dwProId!=-1 )
{
// 不是第一次遍历
printf("\n进程%d的线程总数:%d\n", dwProId, unCount);
unCount = 0;
printf("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n\n");
} dwProId = stcThreadInfo.th32OwnerProcessID;
printf("进程%d:\n\n ", dwProId);
printf("线程TID\t\t线程所属进程PID\t\t线程优先级\n");
} printf(" %d\t\t\t%d\t\t\t %d\n",stcThreadInfo.th32ThreadID, stcThreadInfo.th32OwnerProcessID, stcThreadInfo.tpBasePri);
unCount++; bRet = Thread32Next(hSnapshort, &stcThreadInfo);
} printf("\n进程%d的线程总数:%d\n", dwProId, unCount);
unCount = 0;
printf("=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=\n\n\n"); CloseHandle(hSnapshort); system("pause");
return 0;
}

在win程序设计中除了使用ToolHelp函数可以完成进程的遍历操作之外(程序需要包含tlhelp32.h),还可以使用Psapi函数EnumProcess函数,但是它只能获取进程的ID。

进程相关操作--终止进程

  1. void  WINAPI  ExitProcess(
  2. UINT  uExitCode;  // 进程退出码
  3. );
void  WINAPI  ExitProcess(
UINT uExitCode; // 进程退出码
);

在程序的任意一个地方调用这个函数都会立即终止自身进程的运行。在C/C++中应避免直接调用这个函数,因为C/C++运行期库得不到通知,∴没有机会去调用全局或静态C/C++对象的析构函数。

ExitProcess不能终止其他进程,应使用TerminateProcess函数。
  1. BOOL WINAPI TerminateProcess(
  2. HANDLE hProcess,  // 要结束的进程句柄
  3. UINT uExitCode    // 指定目标进程的退出码
  4. );
BOOL WINAPI TerminateProcess(
HANDLE hProcess, // 要结束的进程句柄
UINT uExitCode // 指定目标进程的退出码
);

我们必须通过获取目标进程的ID,然后再获取这个进程句柄,然后才可以使用这个句柄操作此进程。可以使用如下函数:

  1. HANDLE WINAPI OpenProcess(
  2. DWORD dwDesiredAccess,  // 指定得到的句柄具有的访问权限
  3. BOOL  bInheritHandle,   // 指定返回的句柄是否可被继承
  4. DWORD dwProcessId       // 指定要打开的进程ID
  5. );
HANDLE WINAPI OpenProcess(
DWORD dwDesiredAccess, // 指定得到的句柄具有的访问权限
BOOL bInheritHandle, // 指定返回的句柄是否可被继承
DWORD dwProcessId // 指定要打开的进程ID
);

这个函数打开一个存在的进程,返回具有指定权限的句柄。

获取进程的其他信息

  • 获取进程对应的可执行程序路径
  1. DWORD  WINAPI  GetProcessImageFileNameA (
  2. _In_ HANDLE hProcess,                       // 要获得文件路径的进程句柄
  3. _Out_writes_(nSize) LPSTR lpImageFileName,  // 保存文件路径的内存首地址
  4. _In_ DWORD nSize<span style="white-space:pre">              </span>// 保存文件路径的内存大小
  5. );
DWORD  WINAPI  GetProcessImageFileNameA (
_In_ HANDLE hProcess, // 要获得文件路径的进程句柄
_Out_writes_(nSize) LPSTR lpImageFileName, // 保存文件路径的内存首地址
_In_ DWORD nSize// 保存文件路径的内存大小
);

注意使用上述函数获取的路径并不是磁盘驱动器的路径,而是内核路径,形如: " \Device\HarddiskVolume1\WINDOWS\System32\notepad.exe ",通常此路径中的Device表示硬盘驱动器,HarddiskVolume表示分区数字表示具体分区号(1表C 2表D。。。),所以对路径需要进行转换。

  1. #include <stdio.h>
  2. #include <windows.h>
  3. #include <tchar.h>
  4. #include <TlHelp32.h>
  5. #include <Psapi.h>
  6. #pragma comment(lib, "psapi.lib")
  7. void TransPath(PTCHAR pPath)
  8. {
  9. // 获取驱动器字符串位置
  10. PTCHAR pFind = _tcsstr(pPath, _T("HarddiskVolume"));
  11. if( pFind==NULL )
  12. return;
  13. TCHAR tcDriver[5] = {'C', 'D', 'E', 'F', 'G'};  // 可写满24个字符,这里只是一个demo
  14. int nNum = pFind[_tcslen(_T("HarddiskVolume"))] - 0x30;
  15. PTCHAR pTemp = _tcsstr(pFind, _T("\\"));
  16. // 拼接字符串的缓冲区
  17. TCHAR tcBuffer[MAX_PATH];
  18. memset(tcBuffer, 0, MAX_PATH*sizeof(TCHAR));
  19. _stprintf_s(tcBuffer, _T("%c:%s"), tcDriver[nNum-1], pTemp);
  20. memset(pPath, 0, _tcslen(pPath));
  21. _tcscpy_s(pPath, 256, tcBuffer);
  22. }
  23. int main()
  24. {
  25. // 传入\Device\HarddiskVolume1\WINDOWS\System32\notepad.exe
  26. DWORD dwNeed;
  27. PDWORD pdwMem = new DWORD[4000];
  28. BOOL bRet = EnumProcesses(pdwMem, 4000, &dwNeed);
  29. if( !bRet )
  30. {
  31. if(pdwMem!=NULL)
  32. {
  33. delete[] pdwMem;
  34. pdwMem = nullptr;
  35. return -1;
  36. }
  37. }
  38. int nNumProc = dwNeed/4;
  39. HANDLE hProcess = NULL;
  40. DWORD dwPathLength = 0;
  41. TCHAR tcBuffer[256] = {0};
  42. for( int i=0; i<nNumProc; i++ )
  43. {
  44. hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pdwMem[i]);
  45. if( hProcess==NULL )
  46. continue;
  47. memset(tcBuffer, 0, 256*sizeof(TCHAR));
  48. dwPathLength = GetProcessImageFileName(hProcess, tcBuffer, 256);
  49. if( dwPathLength!=0 )
  50. {
  51. TransPath(tcBuffer);
  52. printf("%08d %ls\n",pdwMem[i], tcBuffer);
  53. }
  54. CloseHandle(hProcess);
  55. }
  56. if( pdwMem!=NULL )
  57. {
  58. delete[] pdwMem;
  59. pdwMem = NULL;
  60. }
  61. system("pause");
  62. return 0;
  63. }
#include <stdio.h>
#include <windows.h>
#include <tchar.h>
#include <TlHelp32.h>
#include <Psapi.h> #pragma comment(lib, "psapi.lib") void TransPath(PTCHAR pPath)
{
// 获取驱动器字符串位置
PTCHAR pFind = _tcsstr(pPath, _T("HarddiskVolume"));
if( pFind==NULL )
return; TCHAR tcDriver[5] = {'C', 'D', 'E', 'F', 'G'}; // 可写满24个字符,这里只是一个demo int nNum = pFind[_tcslen(_T("HarddiskVolume"))] - 0x30; PTCHAR pTemp = _tcsstr(pFind, _T("\\")); // 拼接字符串的缓冲区
TCHAR tcBuffer[MAX_PATH];
memset(tcBuffer, 0, MAX_PATH*sizeof(TCHAR)); _stprintf_s(tcBuffer, _T("%c:%s"), tcDriver[nNum-1], pTemp);
memset(pPath, 0, _tcslen(pPath));
_tcscpy_s(pPath, 256, tcBuffer);
} int main()
{
// 传入\Device\HarddiskVolume1\WINDOWS\System32\notepad.exe
DWORD dwNeed;
PDWORD pdwMem = new DWORD[4000];
BOOL bRet = EnumProcesses(pdwMem, 4000, &dwNeed); if( !bRet )
{
if(pdwMem!=NULL)
{
delete[] pdwMem;
pdwMem = nullptr;
return -1;
}
} int nNumProc = dwNeed/4;
HANDLE hProcess = NULL;
DWORD dwPathLength = 0;
TCHAR tcBuffer[256] = {0}; for( int i=0; i<nNumProc; i++ )
{
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pdwMem[i]); if( hProcess==NULL )
continue; memset(tcBuffer, 0, 256*sizeof(TCHAR));
dwPathLength = GetProcessImageFileName(hProcess, tcBuffer, 256); if( dwPathLength!=0 )
{
TransPath(tcBuffer);
printf("%08d %ls\n",pdwMem[i], tcBuffer);
} CloseHandle(hProcess);
} if( pdwMem!=NULL )
{
delete[] pdwMem;
pdwMem = NULL;
} system("pause");
return 0;
}
  • 获取进程内模块信息
一个可执行程序要执行就必须运行起来形成一个进程,即把自身加载到内存,我们把这段内存称为该进程的主模块。but并不是程序的所有功能都位于主模块中,也有部分功能位于动态链接库中,程序运行后也会把辅助功能的DLL加载到内存形成其他的模块。摒弃一个进程中可以存在多个模块用于辅助主模块完成各种任务。有些病毒把自身代码以模块形式加载到正常进程中去然后偷偷执行。
使用ToolHelp系列函数获取进程模块和遍历进程模块
首先给某个进程的模块拍个快照,这样即可得到这个进程的模块列表,里面记录了每个模块的详细信息。然后使用Module32FirstModule32Next即可遍历快照列表中记录的模块信息。
使用Psapi函数操作
使用EnumProcessModules函数。这个函数只能得到每个模块的句柄,不能得到模块的名称和路径,所以需要使用GetModuleFileNameEx函数通过模块句柄去获得模块的路径。在枚举进程中的模块时,第一个模块是进程的主模块,用主模块句柄得到的模块路径就是进程文件的路径。
  • 获取进程命令行参数
命令行参数即进程启动时传递给它的参数,通过这个参数可知道金城个启动情况。例如系统服务中,凡是服务对应模块是DLL文件情况,都需要svchost.exe去加载启动,这样系统中就有很多svchost进程,每个svchost进程对应一类服务,这时就是通过命令行参数来区分他们。获取本进程命令行参数API:

  1. // 返回命令行参数字符串的首地址
  2. LPTSTR  GetCommandLine(VOID);
// 返回命令行参数字符串的首地址
LPTSTR GetCommandLine(VOID);

获取指定进程的命令行参数,可以使用OD随意调试一个Win程序,如notepad.exe。在command窗口输入follow GetCommandLineA命令即可定位到该函数的实现代码。可以看到实现代码仅一行

  1. mov  ax, dword ptr [7C8835F4]
  2. // 个人做测试的时候地址好像是7C8855F4
mov  ax, dword ptr [7C8835F4]
// 个人做测试的时候地址好像是7C8855F4

也就是说进程的命令行参数被保存在内存中的固定位置,只要定位到GetCommandLineA的入口地址,跳过一个字节后取出一个dword值,这个值是一个指针,指向的内容即命令行参数的地址。现在的问题即得到远程进程中GetCommandLineA函数的入口地址即可得到该进程的命令行参数。获取指定模块中某个Win32API函数地址的函数是GetProcAddress。but这个函数仅可获取当前进程中API函数的地址,要获得远程进程中某个函数的地址就必须在远程进程中执行这个函数,需要使用远线程方法,本文暂时不涉及这个内容。换个思路:GetCommandLineA使用Kernel32.dll提供,这个库是系统基本库,几乎所有程序都会加载这个模块。而且对于NT架构所有进程加载Kernel32.dll的模块基址基本都是一致的,而GetCommandLineA在其库中的地址也是固定的。所以我们在本进程中得到的此函数地址和远程进程中该函数的地址是一致的(这个方法在我的电脑Win7x64中实验失败,应该因为程序的加载基址不固定了)。获取远程进程中数据使用ReadProcessMemory函数。

  1. BOOL  WINAPI  ReadProcessMemory(
  2. _In_ HANDLE hProcess, <span style="white-space:pre">                    </span>         // 远程进程句柄
  3. _In_ LPCVOID lpBaseAddress,<span style="white-space:pre">                       </span> // 目标进程内存空间首地址
  4. _Out_writes_bytes_to_(nSize, *lpNumberOfBytesRead) LPVOID lpBuffer,  // 本进程内存空间首地址,存储读出的数据
  5. _In_ SIZE_T nSize,<span style="white-space:pre">                            </span> // 要读取的字节数
  6. _Out_opt_ SIZE_T * lpNumberOfBytesRead<span style="white-space:pre">                </span> // 实际读取的字节数
  7. );
BOOL  WINAPI  ReadProcessMemory(
_In_ HANDLE hProcess, // 远程进程句柄
_In_ LPCVOID lpBaseAddress, // 目标进程内存空间首地址
_Out_writes_bytes_to_(nSize, *lpNumberOfBytesRead) LPVOID lpBuffer, // 本进程内存空间首地址,存储读出的数据
_In_ SIZE_T nSize, // 要读取的字节数
_Out_opt_ SIZE_T * lpNumberOfBytesRead // 实际读取的字节数
);

为保证此方法成功,应判断当前系统是否是NT系统,使用GetVersion函数获取当前系统的版本。返回值表示系统的版本信息,若是NT/2k/xp则返回值小于0x80000000,否则大于该值。虚拟机中XP实验成功。

jpg改rar 

获取系统中所有进程&线程信息的更多相关文章

  1. Android中获取系统上安装的APP信息

    Version:0.9 StartHTML:-1 EndHTML:-1 StartFragment:00000099 EndFragment:00003259 Android中获取系统上安装的APP信 ...

  2. Linux 系统中僵尸进程

    Linux 系统中僵尸进程和现实中僵尸(虽然我也没见过)类似,虽然已经死了,但是由于没人给它们收尸,还能四处走动.僵尸进程指的是那些虽然已经终止的进程,但仍然保留一些信息,等待其父进程为其收尸.配图源 ...

  3. [linux]top命令详解-实时显示系统中各个进程的资源占用状况

    简介 top命令是Linux下常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于Windows的任务管理器. top显示系统当前的进程和其他状况,是一个动态显示过程,即可以通过用户按 ...

  4. 9.7 top:实时显示系统中各个进程的资源占用状况

    top命令 用于实时地对系统处理器状态进行监控,它能够实时地显示系统中各个进程的资源占用状况.该命令可以按照CPU的使用.内存的使用和执行时间对系统任务进程进行排序显示,同时top命令还可以通过交互式 ...

  5. Unix/Linux系统中僵尸进程是如何产生的?有什么危害?如何避免?

    如题 Unix/Linux系统中僵尸进程是如何产生的?有什么危害?如何避免? 一个进程在调用exit命令结束自己的生命的时候,其实他并没有真正的被销毁,而是留下一个称为僵尸进程(Zombie)的数据结 ...

  6. java中的getProperty()方法。获取系统中属性名为key的属性对应的值

    总结:getProperty方法:获取系统中属性名为key的属性对应的值,系统中常见的属性名以及属性如下: 现在用getProperty()的方法,获取系统信息代码: package com.aaa; ...

  7. linux系统中的进程状态分析

    转载地址:https://blog.csdn.net/shenwansangz/article/details/51981459 linux是一个多用户,多任务的系统,可以同时运行多个用户的多个程序, ...

  8. 显示系统中所有的socket信息

    netstat -aon /proc/net/tcp /proc/net/udp /proc/net/unix 相关的代码是:tcp4_seq_show(struct seq_file *file, ...

  9. linux系统中的进程

    一.fork 在类unix系统中,我们所执行的任何程序,都是由父进程(parent process)所产生出来的一个子进程(child process),子进程在结束后,将返回到父进程去.此一现象被称 ...

随机推荐

  1. Oracle数据库找回密码

    Oracle数据库忘记用户的密码.经验证,可行的解决方案如下: 1.Ctrl + R 打开cmd窗口,输入 sqlplus / as sysdba  (注意/左右两侧有空格) 2.运行cmd ,输入 ...

  2. WCF 服务编程 - 常用绑定

    WCF  定义了5中常用的绑定. 一. 绑定 1.基本绑定: 对应于BasicHttpBinding类.基本绑定能够将WCF服务公开为传统的ASMX Web服务,使得原客户端能够与新的服务协作.如果客 ...

  3. Oracle 如何扩展表空间

    第一步:查看ORACLE表空间的使用情况:SELECT DBF.TABLESPACE_NAME,       DBF.TOTALSPACE "总量(M)",       DBF.T ...

  4. linux 下安装mongodb

    1.初始化docker:    -v 设置docker和host共享目录,格式hostPath:dockerContainerPath    -p 端口映射    --name,容器名称    cen ...

  5. iOS 25个性能优化/内存优化常用方法

    1. 用ARC管理内存 ARC(Automatic ReferenceCounting, 自动引用计数)和iOS5一起发布,它避免了最常见的也就是经常是由于我们忘记释放内存所造成的内存泄露.它自动为你 ...

  6. [BZOJ1552][Cerc2007]robotic sort

    [BZOJ1552][Cerc2007]robotic sort 试题描述 输入 输入共两行,第一行为一个整数N,N表示物品的个数,1<=N<=100000.第二行为N个用空格隔开的正整数 ...

  7. 3篇NeuroImage文献分析

    鉴于之前读的一些文章很容易就忘掉了,故打算花点时间记录下所读的文献. 这几天花了一些时间读了3篇文献: Intersubject consistency of cortical MEG signals ...

  8. 163邮件出错:不允许使用邮箱名称。 服务器响应为: authentication is required,smtp7,C8CowEDpS0+Uke9VvSmXBg--.546S2 1441763733

    原因:用163邮箱发邮件,需开启smtp服务,开启服务时,要求使用客户端授权码. 在.net中,使用smtp发邮件,在验证中使用的密码,是上面所讲的客户端授权码,而不是注册和web登录时用的邮箱密码. ...

  9. nginx负载SignalR

    前几天写了篇聊天室服务器扩展随想,今天有空开始实施第一步, 聊天服务器用SignalR self-host,负载用nginx,当然这只是测试,实际使用可能还需要修改. 第一步,搭好SignalR服务, ...

  10. [MySQL] 分页优化

    在传统的分页思路影响下,很多人都形成了对于分页的固定理解,也就是给出select语句,先用count()函数计算出总的条目,除与每个页面大小pagesize,然后用ceil取整,得出总的页数,用lim ...