【第45节】windows程序的其他反调试手段上篇

目录

引言

一、通过窗口类名和窗口名判断

二、检测调试器进程

三、父进程是否是Explorer

四、RDTSC/GetTickCount时间敏感程序段

五、StartupInfo结构的使用

六、使用BeingDebugged字段

七、 PEB.NtGlobalFlag,Heap.HeapFlags,Heap.ForceFlags

八、DebugPort:CheckRemoteDebuggerPresent()/NtQueryInformationProcess()


引言

        在Windows程序的开发与安全领域中,反调试技术是保障程序安全、防止被恶意调试分析的重要手段。上一节我们已经了解了一些相关的反调试方法,而这还远远不够。接下来我们将进一步探索更多有效的反调试技巧,篇幅分上中下三篇来讲述,该节是第一篇。

一、通过窗口类名和窗口名判断

        FindWindow函数:使用`FindWindow`函数,能依据特定的类名或者窗口名去查找对应的窗口。
        EnumWindow函数:调用`EnumWindow`函数后,系统会逐个遍历所有的顶级窗口,每遍历到一个窗口,就会调用一次回调函数。在这个回调函数里,先用`GetWindowText`函数获取窗口的标题,接着利用`strstr`函数(该函数区分大小写,与之对应的`StrStrI`函数不区分大小写)等,在窗口标题中查找是否存在“ollydbg”这样的字符串。`StrStr`函数的作用是返回`str2`首次在`str1`中出现的位置,要是没找到,就会返回`NULL`。
        GetForeGroundWindow函数:`GetForeGroundWindow`函数会返回桌面上当前处于激活状态的窗口。当程序处于被调试状态时,调用这个函数能够获取到“ollydbg”窗口的句柄,得到句柄后,就能向这个窗口发送`WM_CLOSE`消息,从而将其关闭。

关键示例代码:

 // FindWindow相关代码void CDetectODDlg::OnWndcls() {HWND hWnd;if (hWnd = ::FindWindow("ollyDbg", NULL)) {MessageBox("   发现OD");::SendMessage(hWnd, WM_CLOSE, NULL, NULL);}else {MessageBox("   没发现OD");}}// EnumWindow相关代码// 包含头文件:#include "Shlwapi.h"BOOL CALLBACK EnumWindowsProc(//handle to parent windowLPARAM lParam //application - defined value) {char ch[100];CString str = "ollydbg";if (IsWindowVisible(hwnd)) {::GetWindowText(hwnd, ch, 100);//AfxMessageBox(ch);if (::StrStrI(ch, str)) {AfxMessageBox("发现OD");return FALSE;}}return TRUE;}void CDetectODDlg::OnEnumwindow() {EnumWindows(EnumWindowsProc, NULL);AfxMessageBox("枚举窗口结束,未提示发现OD,则没有OD");}

二、检测调试器进程

        为了防范逆向分析人员通过修改调试器可执行文件名来规避检测,可采取以下方法:先对进程列表进行枚举操作,在这个过程中仔细查看是否有诸如“OLLYDBG.EXE”“windbg.exe”这类常见调试器进程存在。

        同时,借助`kernel32!ReadProcessMemory()`函数读取各个进程的内存数据,在读取到的数据里查找像“OLLYDBG”这样与调试器相关的字符串。一旦发现相关进程或字符串,就可以判定可能存在调试行为。 

关键示例代码:

 // 需要头文件:#include"tlhelp32.h"void CDetectODDlg::OnEnumProcess() {HANDLE hwnd;PROCESSENTRY32 tp32; //结构体CString str = "OLLYDBG.EXE";BOOL bFindOD = FALSE;hwnd = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);if (INVALID_HANDLE_VALUE!= hwnd) {Process32First(hwnd, &tp32);do {if (0 == lstrcmp(str, tp32.szExeFile)) {AfxMessageBox("发现OD");bFindOD = TRUE;break;}} while (Process32Next(hwnd, &tp32));if (!bFindOD)AfxMessageBox("没有OD");}CloseHandle(hwnd);}

三、父进程是否是Explorer

- 原理说明:一般来说,当我们通过双击的方式来运行程序时,这个程序进程的父进程通常是`explorer.exe`。要是情况并非如此,那就很有可能意味着该程序正处于被调试的状态。
   - 具体实现步骤:
     1. 可以通过`TEB(TEB.ClientId)`,也能使用`GetCurrentProcessId()`函数,来获取当前进程的PID(进程标识符)。
     2. 运用`Process32First/Next()`函数,能够获取到所有进程的列表。在这个列表里,要留意`explorer.exe`对应的PID,它可从`PROCESSENTRY32.szExeFile`中获取,同时还要关注当前进程的父进程PID,这个可通过`PROCESSENTRY32.th32ParentProcessID`来获取。另外,`Explorer`进程的ID还可以依据桌面窗口的类和名称来得到。
     3. 要是父进程的PID和`explorer.exe`、`cmd.exe`、`Services.exe`的PID都不相同,那么这个目标进程极有可能正在被调试。
   - 应对策略:`Olly Advanced`采用的办法是让`Process32Next()`函数一直返回`fail`,这样进程枚举就无法正常进行,PID检查也就会被跳过。实现这一操作,是通过对`kernel32!Process32NextW()`的入口代码打补丁(把`EAX`值设置为0后直接返回)来达成的。

   // (1)通过桌面类和名称获得Explorer的PID源码DWORD ExplorerID;::GetWindowThreadProcessId(::FindWindow("Progman", NULL), &ExplorerID);// (2)通过进程列表快照获得Explorer的PID源码void CDetectODDlg::OnExplorer() {HANDLE hwnd;PROCESSENTRY32 tp32; //结构体CString str = "Explorer.EXE";DWORD ExplorerID;DWORD SelfID;DWORD SelfParentID;SelfID =GetCurrentProcessId();::GetWindowThreadProcessId(::FindWindow("Progman", NULL), &ExplorerID);hwnd = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);if (INVALID_HANDLE_VALUE!= hwnd) {Process32First(hwnd, &tp32);do {if (0 == lstrcmp(str, tp32.szExeFile)) {//ExplorerID=tp32.th32ProcessID;//AfxMessageBox("aaa");}if (SelfID == tp32.th32ProcessID) {SelfParentID = tp32.th32ParentProcessID;}} while (Process32Next(hwnd, &tp32));str.Format("本进程:%d父进程:%d Explorer进程:%d", SelfID, SelfParentID, ExplorerID);MessageBox(str);if (ExplorerID == SelfParentID)AfxMessageBox("没有OD");elseAfxMessageBox("发现OD");CloseHandle(hwnd);}}

四、RDTSC/GetTickCount时间敏感程序段

原理阐述

        在进程正常运行时,CPU循环相对稳定。然而,一旦进程处于被调试状态,调试器的事件处理代码以及步过指令等操作会占用CPU资源,这就使得进程中相邻指令执行所耗费的时间大幅增加。因此,若发现相邻指令间耗时远超正常水平,基本可以判定该进程正被调试。

RDTSC操作说明

        `RDTSC`指令的功能是将计算机自启动以来CPU的运行周期数存储到`EDX:EAX`中,其中`EDX`存储高位数据,`EAX`存储低位数据。但要注意,如果`CR4`中的`TSD(time stamp disabled)`标志被置位,那么在`ring3`级别运行`rdtsc`指令会引发异常,因为这属于特权指令。为解决这个问题,需要进入`ring0`级别,将该标志置位。接着,对`OD`(OllyDbg调试器)的`WaitForDebugEvent`进行`Hook`操作,以此拦截异常事件。当捕获到异常代码属于特权指令时,读取异常处的操作码(`opcode`)进行检查。若确定是`rdtsc`指令,将指令指针(`eip`)值增加2,并通过`SetThreadContext`函数进行相关设置,此时`edx:eax`的返回值可由开发者自主决定。

GetTickCount方法

        获取当前系统时间,其原理是:当程序被调试时,调试器的事件处理代码、单步执行等操作会占用 CPU 时间,导致代码执行的时间变长。通过记录两段代码执行前后的系统时间(以毫秒为单位),计算时间差,若这个时间差超过了正常情况下的预期值(这里设定为 100 毫秒),就认为程序可能正在被调试,即检测到了调试器。

关键代码示例:

 void CDetectODDlg::OnGetTickCount() {//TOD0:Add your control notification handler code hereDWORD dTimel;DWORD dTime2;dTimel =GetTickCount();GetCurrentProcessId();GetCurrentProcessId();GetCurrentProcessId();GetCurrentProcessId();dTime2 =GetTickCount();if (dTime2 - dTimel>100) {AfxMessageBox("发现OD");}else {AfxMessageBox("没有OD");}}

五、StartupInfo结构的使用

        原理说明:在Windows操作系统里,当`explorer.exe`创建进程时,它会将`STARTUPINFO`结构里的值设置为0 。然而,要是进程不是由`explorer.exe`创建的,那么在创建过程中就会忽略`STARTUPINFO`结构中的值,这就意味着该结构里的值不会是0 。基于这个特性,我们能够以此来判断是否有像OD这样的调试器正在调试程序。 

关键示例代码:

   // 结构体定义typedef struct _STARTUPINFO {DWORD cb;           // 0000PSTR lpReserved;       // 0004PSTR lpDesktop;        // 0008PSTR lpTitle;           // 000DDWORD dwX;          // 0010DWORD dwY;          // 0014DWORD dwXSize;       // 0018DWORD dwYSize;       // 001DDWORD dwXCountChars; // 0020DWORD dwYCountChars; // 0024DWORD dwFillAttribute;  // 0028DWORD dwFlags;        // 002DWORD wShowWindow;   // 0030WORD cbReserved2;      // 0034PBYTE lpReserved2;      // 0038HANDLE hStdInput;      // 003DHANDLE hStd0utput;     // 0040HANDLE hStdError;       // 0044} STARTUPINFO, * LPSTARTUPINFO;void CDetectODDlg::OnGetStartupInfo() {STARTUPINFO info;GetStartupInfo(&info);if (info.dwX!= 0 || info.dwY!= 0|| info.dwXCountChars!= 0 || info.dwYCountChars!= 0|| info.dwFillAttribute!= 0 || info.dwXSize!= 0 || info.dwYSize!= 0)AfxMessageBox("发现OD");elseAfxMessageBox("没有OD");}

六、使用BeingDebugged字段

原理阐述

        在Windows系统环境下,存在一个名为`kernel32!IsDebuggerPresent()`的API函数,它的作用是通过检测进程环境块(PEB)里的`BeingDebugged`标志,来判断当前进程是否正被用户模式的调试器调试。每个进程都有自己的PEB结构,一般来说,获取PEB地址得借助线程环境块(TEB)。具体来说,`Fs:[0]`这个地址指向的是当前线程的TEB结构。在TEB结构里,偏移量为0的位置是线程信息块结构TIB 。而在TIB结构中,偏移18H的地方有个`self`字段,它其实是TIB的反身指针,这个指针又指向TIB(同时也是PEB)的起始地址。另外,在TEB偏移30H处,有一个指针指向PEB结构。在PEB结构里,偏移2H的位置就是`BeingDebugged`字段,其数据类型为`Uchar` 。

检测方式介绍

        1. 调用函数间接读取:可以调用`IsDebuggerPresent`函数,该函数会间接地读取`BeingDebugged`字段的值,以此判断进程是否被调试。
        2. 直接地址读取:也能通过获取的地址,直接去读取`BeingDebugged`字段,进而判断进程的调试状态 。

应对策略说明

        1. 手动修改标志值:在数据窗口中,通过按下`Ctrl + G`组合键,输入`fs:[30]`,就可以查看PEB数据。找到`PEB.BeingDebugged`标志后,将其值设置为0 。
        2. 使用Ollyscript命令:利用`Ollyscript`中的“`dbh`”命令,能够对`PEB.BeingDebugged`这个标志进行补丁操作 。

关键代码示例:

   void CDetectODDlg::OnIsdebuggerpresent() {if (IsDebuggerPresent())MessageBox("发现OD");elseMessageBox("没有OD");}

七、 PEB.NtGlobalFlag,Heap.HeapFlags,Heap.ForceFlags

        PEB.NtGlobalFlag情况:一般情况下,程序没被调试时,PEB里有个成员叫`NtGlobalFlag`(偏移量是0x68),它的值是0。要是进程正在被调试,这个值通常会变成0x70,这代表下面这些标志被设置了:
        - `FLG_HEAP_ENABLE_TAIL_CHECK(0X10)`
        - `FLG_HEAP_ENABLE_FREE_CHECK(0X20)`
        - `FLG_HEAP_VALIDATE_PARAMETERS(0X40)`
        这些标志是在`ntdll!LdrpInitializeExecutionOptions()`函数里进行设置的。要注意,`PEB.NtGlobalFlag`的默认值可以通过`gflags.exe`工具来修改,也能在注册表的`HKLM\Software\Microsoft\Windows Nt\CurrentVersion\Image File Execution Options`位置创建条目进行修改。下面是相关的汇编代码:

mov eax,fs:[30h]
mov eax,[eax+68h]
and eax,70h

        堆标志情况:因为设置了`NtGlobalFlag`标志,堆也会开启几个标志,这个变化能在`ntdll!Rt1CreateHeap()`函数里观察到。正常情况下,系统给进程创建第一个堆时,会把`Flags`设为2(也就是`HEAP_GROWABLE`),把`ForceFlags`设为0。要是进程正在被调试,这两个标志通常会分别设为50000062(具体数值取决于`NtGlobalFlag`)和0x40000060(这个值等于`Flags AND 0x6001007D`)。下面是相关的汇编代码:

assume fs:nothing
mov ebx,fs:[30h]     ;ebx指向PEB
mov eax,[ebx+18h]   ;PEB.ProcessHeap
cmp dword ptr [eax+0ch],2    ;PEB.ProcessHeap.Flags
jne debugger_found
cmp dword ptr [eax+10h],0          ;PEB.ProcessHeap.ForceFlags
jne debugger_found

        这些标志位的变化都是由`BeingDebugged`引发的。系统在创建进程时,会将`BeingDebugged`设为`TRUE`。之后,`NtGlobalFlag`会依据这个标记,设置如`FLGVALIDATEPARAMETERS`等标记。在为进程创建堆时,受`NtGlobalFlag`影响,堆的`Flags`会被设置一些标记。这些标记随后会被填入`ProcessHeap`的`Flags`和`ForceFlags`中。同时,堆里会被填入很多类似“BAADFOOD”的内容(也就是`HeapMagic`,也能用于检测调试情况)。若要一次性解决这些状态相关问题,可查看《加密解密》413页。 

关键示例代码:

 typedef NTSTATUS(_stdcall *ZwQueryInformationProcess)(HANDLE ProcessHandle,PROCESSINFOCLASS ProcessInformationClass,PVOID ProcessInformation,ULONG ProcessInformationLength,PULONG ReturnLength);//定义函数指针void CDetectODDlg::OnPebFlags() {//定义函数指针变量ZwQueryInformationProcess MyZwQueryInformationProcess;HANDLE hProcess = NULL;PROCESS_BASIC_INFORMATION pbi = {0};LLONG peb = 0;LLONG cnt = 0;LLONG PebBase = 0;LLONG AddrBase;BOOL blFoundOD = FALSE;WORD flag;DWORD dwFlag;DWORD bytesrw;DWORD Processld = GetCurrentProcessId();hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, Processld);if (hProcess!= NULL) {//函数指针变量赋值MyZwQueryInformationProcess=(ZwQueryInformationProcess)GetProcAddress(LoadLibrary("ntdll.dll"),"ZwQueryInformationProcess");//函数指针变量调用if (MyZwQueryInformationProcess(hProcess,ProcessBasicInformation,&pbi,sizeof(PROCESS_BASIC_INFORMATION),&cnt) == 0) {PebBase = (ULONG)pbi.PebBaseAddress;  //获取PEB地址AddrBase = PebBase;//读内存地址if (ReadProcessMemory(hProcess, (LPCVOID)(PebBase + 0x68), &flag, 2, &bytesrw) && bytesrw == 2) {//PEB.NtGlobalFlagif (0x70 == flag) {bFoundOD = TRUE;if (ReadProcessMemory(hProcess, (LPCVOID)(PebBase + 0x18), &dwFlag, 4, &bytesrw) && bytesrw == 4) {AddrBase = dwFlag;if (ReadProcessMemory(hProcess, (LPCVOID)(AddrBase + 0x0c), &flag, 2, &bytesrw) && bytesrw == 2) {//PEB.ProcessHeap.Flagsif (2!= flag) {bFoundOD = TRUE;if (ReadProcessMemory(hProcess, (LPCVOID)(AddrBase + 0x10), &flag, 2, &bytesrw) && bytesrw == 2) {//PEB.ProcessHeap.Forceflagsif (0!= flag) {bFoundOD = TRUE;}}}}}}if (bFoundOD == FALSE)AfxMessageBox("没有OD");else {AfxMessageBox("发现OD");}}CloseHandle(hProcess);}}}

八、DebugPort:CheckRemoteDebuggerPresent()/NtQueryInformationProcess()

        `Kernel32!CheckRemoteDebuggerPresent()` 这个函数的作用是判断有没有调试器附加到某个进程上。它的函数原型如下: 

BOOL CheckRemoteDebuggerPresent(HANDLE hProcess,PBOOL pbDebuggerPresent
);

        `Kernel32!CheckRemoteDebuggerPresent()` 函数需要 2 个参数。第一个参数是进程句柄,第二个参数是指向 `boolean` 变量的指针。要是进程正在被调试,这个变量就会是 `TRUE`。这个 API 实际上是调用了 `ntdll!NtQuery InformationProcess()` 来完成检测。

        下面定义函数指针:

typedef BOOL(WINAPI*CHECK_REMOTE_DEBUGGER_PRESENT)(HANDLE, PBOOL);

        检测函数的实现:

void CDetectODDlg::OnCheckremotedebuggerpresent() {HANDLE hProcess;HINSTANCE hModule;BOOL bDebuggerPresent = FALSE;CHECK_REMOTE_DEBUGGER_PRESENT CheckRemoteDebuggerPresent; // 建立函数指针变量hModule = GetModuleHandleA("Kernel32"); // 地址要从模块中动态获得CheckRemoteDebuggerPresent = (CHECK_REMOTE_DEBUGGER_PRESENT)GetProcAddress(hModule, "CheckRemoteDebuggerPresent"); // 获取地址hProcess = GetCurrentProcess();CheckRemoteDebuggerPresent(hProcess, &bDebuggerPresent); // 调用if (bDebuggerPresent == TRUE) {AfxMessageBox("发现OD");}else {AfxMessageBox("没有OD");}
}

        `ntdll!NtQueryInformationProcess()` 函数有 5 个参数。要是想检测调试器是否存在,得把 `ProcessInformationclass` 参数设置成 `ProcessDebugPort(7)`。`NtQueryInformationProcess()` 会去获取内核结构 `EPROCESS` 里的 `DebugPort` 成员,这个成员其实是系统和调试器进行通信时用的端口句柄。要是 `DebugPort` 成员的值不是 0,就说明进程正在被用户模式的调试器调试。这种情况下,`ProcessInformation` 会被设为 `0xFFFFFFFF`;要是没有被调试,`ProcessInformation` 就会被设为 0。

        `ZwQueryInformationProcess` 函数的原型如下:

ZwQueryInformationProcess(IN HANDLE ProcessHandle,IN PROCESSINFOCLASS ProcessInformationClass,OUT PVOID ProcessInformation,IN ULONG ProcessInformationLength,OUT PULONG ReturnLength OPTIONAL
);

        定义函数指针如下:

typedef NTSTATUS(__stdcall *ZW_QUERY_INFORMATION_PROCESS)(HANDLE ProcessHandle,PROCESSINFOCLASS ProcessInformationClass, // 该参数也需要上面声明的数据结构PVOID ProcessInformation,ULONG ProcessInformationLength,PULONG ReturnLength
);

        检测函数的实现:

void CDetectODDlg::OnZwqueryinfomationprocess() {//TODO: Add your control notification handler code hereHANDLE hProcess;HINSTANCE hModule;DWORD dwResult;ZW_QUERY_INFORMATION_PROCESS MyFunc;hModule = GetModuleHandle("ntdll.dll");MyFunc = (ZW_QUERY_INFORMATION_PROCESS)GetProcAddress(hModule, "ZwQueryInformationProcess");hProcess = GetCurrentProcess();MyFunc(hProcess, ProcessDebugPort, &dwResult, 4, NULL);if (dwResult != 0) {AfxMessageBox("发现OD");}else {AfxMessageBox("没有OD");}
}

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/87.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

.Net 9 webapi使用Docker部署到Linux

参考文章连接: https://www.cnblogs.com/kong-ming/p/16278109.html .Net 6.0 WebApi 使用Docker部署到Linux系统CentOS 7 - 长白山 - 博客园 项目需要跨平台部署,所以就研究了一下菜鸟如何入门Net跨平台部署,演示使用的是Net 9 webAPi Li…

npm和npx的作用和区别

npx 和 npm 是 Node.js 生态系统中两个常用的工具,它们有不同的作用和使用场景。 1. npm(Node Package Manager) 作用: npm 是 Node.js 的包管理工具,主要用于: 安装、卸载、更新项目依赖(包&a…

个人论坛的测试报告

目录 一、项目的介绍 二、项目功能 三、测试项目 1.编写测试用例​编辑 2.执行部分测试用例 3.自动化测试 1)添加相关Maven依赖(pom.xml) 2)编写论坛系统的界面测试用例 3)编写自动化代码测试部分测试用例 4.性能测试 一、项目的介…

《 C++ 点滴漫谈: 三十三 》当函数成为参数:解密 C++ 回调函数的全部姿势

一、前言 在现代软件开发中,“解耦” 与 “可扩展性” 已成为衡量一个系统架构优劣的重要标准。而在众多实现解耦机制的技术手段中,“回调函数” 无疑是一种高效且广泛使用的模式。你是否曾经在编写排序算法时,希望允许用户自定义排序规则&a…

大联盟(特别版)双端互动平台完整套件分享:含多模块源码+本地部署环境

这是一套结构清晰、功能完整的互动平台组件,适合有开发经验的技术人员进行模块参考、结构研究或本地部署实验使用。 该平台覆盖前端展示、后端服务、移动端资源以及完整数据库,采用模块化架构,整体部署流程简单清晰,适合自研团队参…

spark-SOL简介

Spark-SQL简介 一.Spark-SQL是什么 Spark SQL 是 Spark 用于结构化数据(structured data)处理的 Spark 模块 二.Hive and SparkSQL SparkSQL 的前身是 Shark,Shark是给熟悉 RDBMS 但又不理解 MapReduce 的技术人员提供的快速上手的工具 …

深入理解浏览器的 Cookie:全面解析与实践指南

在现代 Web 开发中,Cookie 扮演着举足轻重的角色。它不仅用于管理用户会话、记录用户偏好,还在行为追踪、广告投放以及安全防护等诸多方面发挥着重要作用。随着互联网应用场景的不断丰富,Cookie 的使用和管理也日趋复杂,如何在保障…

macOS 上使用 Homebrew 安装和配置 frp 客户端

macOS 上使用 Homebrew 安装和配置 frp 客户端 (frpc) 指南 frp (Fast Reverse Proxy) 是一款高性能的反向代理应用,常用于内网穿透。本文将介绍在 macOS 上使用 Homebrew 安装 frpc,并进行配置和管理。 一、安装 frpc 使用 Homebrew 安装(…

【HarmonyOS NEXT】多目标产物构建实践

目录 什么是多产物构建 如何定义多个构建产物 如何在项目中使用 参考文章 什么是多产物构建 在鸿蒙应用开发中,一个应用可定义多个 product,每一个 product 对应一个定制的 APP 包,每个 product 中支持对 bundleName、bundleType、输出产…

腾讯云COS直传,官方后端demo,GO语言转JAVA

腾讯云COS直传,官方后端demo,GO写的,我们台是JAVA所以转一下,已跑通。废话不多说,直接上代码: Controller类如下: import com.ruoyi.web.core.config.CosConfig; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.Ht…

C 语言 第八章 文件操作

目录 文件操作 文件和流的介绍 C 输入 & 输出 C 文件的读写 创建/打开文件 写入文件 fputc 函数 fputs 函数 fprintf 函数 实例: 读取文件 fgets函数 实例: 关闭文件 文件操作 文件和流的介绍 变量、数组、结构体等数据在运行时存储于内存…

C#容器源码分析 --- Dictionary<TKey,TValue>

Dictionary<TKey, TValue> 是 System.Collections.Generic 命名空间下的高性能键值对集合&#xff0c;其核心实现基于​​哈希表​​和​​链地址法&#xff08;Separate Chaining&#xff09;。 .Net4.8 Dictionary<TKey,TValue>源码地址&#xff1a; dictionary…