
简介面向Windows应用启动时出现kernel32.dll缺失、找不到或版本不匹配提示的用户与系统排错人员这份资源提供可直接取用的核心动态链接库文件并配有说明文本与介绍页面便于核对文件用途、适用场景及故障背景。压缩包共3个文件包含1个dll主文件、1个txt说明和1个html介绍页整体约355KB体积轻便适合留存或用于同版本系统间的文件比对。已有3382人学习下载具备一定参考基础。资源围绕kernel32.dll在内存管理、进程与线程创建、文件设备操作、系统调用、错误与异常处理、同步对象及环境变量读取等方面的作用展开可帮助读者理解该组件为何影响程序启动和系统稳定并为判断文件损坏、缺失或版本不匹配提供依据说明文本与页面也有助于确认文件基本信息适合需要定位核心DLL问题的Windows用户和运维人员参考。1. kernel32.dll 不是一个文件而是一份约定异常日志里那个名字到底指什么某天凌晨一个桌面工具的崩溃日志里只剩下一行Faulting module name: KERNEL32.DLL。第一反应往往是系统文件坏了于是重装运行库、扫一遍系统文件、换台机器再跑结果问题照旧。kernel32.dll 是 Windows 用户态程序几乎必然链接的底层模块它把文件、内存、进程、线程、时间、错误码这些最基础的能力封装成一组导出函数进程一启动就会被映射进地址空间。名字出现在崩溃模块里只说明崩溃现场落在它内部真正的原因多半在调用方传错的结构体、失效的句柄、错配的位数。这篇笔记按“看清导出表、写通调用、定位故障、做兼容转发”的路子走一遍写给要写 Windows 原生调用、排查加载失败、给跨版本环境做适配的从业者。2. 拆开导出表看约定kernel32.dll 到底对外承诺了什么很多人对 kernel32.dll 的印象停留在“系统自带的一个 DLL”但在链接器和加载器眼里它是一张有明确边界的符号表哪些名字存在、哪些只有序号、哪些函数其实只是转发到别处。把这张表打出来后面所有“为什么调不到”“为什么崩溃在它里面”的问题才有讨论的基础。2.1 导出目录的三个 RVA 与一张名字表PE 文件里描述导出符号的结构叫导出目录位于可选头数据目录数组的第 0 项。定位路径是固定的文件偏移0x3C处存放 NT 头偏移也就是常说的e_lfanew该偏移处是PE\0\0签名紧跟其后是 20 字节的 COFF 文件头里面有节区数量NumberOfSections和可选头长度SizeOfOptionalHeader再往后是可选头开头两字节的魔数决定它是 PE320x10B还是 PE320x20B两者的数据目录起始偏移不同分别是 96 和 112。数据目录第 0 项是一个 8 字节结构前半是导出目录的 RVA后半是它的大小。导出目录本身 40 字节前 12 字节是特征、时间戳和版本号从第 12 字节开始依次是模块名 RVA、序号基数Base、函数总数、名字总数以及三张表的 RVA函数地址表、名字表、名字序号表。三者的关系是理解导出的关键——名字表是一串指向 ASCII 字符串的 RVA 数组名字序号表是等长的 WORD 数组存的是该名字在函数地址表里的下标函数地址表才是真正存函数 RVA 的地方。由此得到两条实用结论。其一AddressOfFunctions的长度由函数总数决定而名字数量通常小于等于函数数量差额就是那些“只有序号、没有名字”的导出项它们只能按序号调用。其二如果某个函数的 RVA 落在导出目录自身的[起始 RVA, 起始 RVA 大小)区间内那它指向的不是代码而是一个字符串形如NTDLL.RtlAllocateHeap这就是转发导出。kernel32.dll 里存在大量转发项现代系统上许多实现已经搬到下层模块kernel32 只保留一个同名出口维持二进制兼容。这一点解释了为什么在调试器里看到调用栈落在别的模块、而导入表里写的却是 kernel32。2.2 用四十行 Python 把导出清单打印出来不依赖第三方库也能把这张表完整读出来只需要struct做定长解包。下面这段脚本接受一个 DLL 路径输出序号、名字、函数 RVA 和转发目标。import struct import sys def rva_to_offset(rva, sections): RVA 换文件偏移sections 每项为 (VA, VirtualSize, RawPtr, RawSize) for va, vsize, raw_ptr, raw_size in sections: span max(vsize, raw_size) # 文件对齐可能大于虚拟大小取大值 if va rva va span: return raw_ptr (rva - va) return None def read_cstr(data, off): end data.index(b\x00, off) return data[off:end].decode(ascii, replace) def dump_exports(path): with open(path, rb) as fp: data fp.read() # 1) DOS 头0x3C 处存放 NT 头偏移 e_lfanew struct.unpack_from(I, data, 0x3C)[0] if data[e_lfanew:e_lfanew 4] ! bPE\0\0: raise SystemExit(not a PE file) # 2) COFF 头节区数量 / 可选头长度 coff e_lfanew 4 _machine, nsec, _ts, _sym, _nsym, opt_size, _ch \ struct.unpack_from(HHIIIHH, data, coff) # 3) 可选头魔数决定数据目录起点 opt coff 20 magic struct.unpack_from(H, data, opt)[0] dd opt (96 if magic 0x10B else 112) exp_rva, exp_size struct.unpack_from(II, data, dd) # 目录项 0 导出 # 4) 节区表用于后续 RVA 换算 sections [] sec_base opt opt_size for i in range(nsec): p sec_base i * 40 vsize, va, raw_size, raw_ptr struct.unpack_from(IIII, data, p 8) sections.append((va, vsize, raw_ptr, raw_size)) # 5) 导出目录跳过前 12 字节取 Base 与三张表 exp_off rva_to_offset(exp_rva, sections) _name, base, nfunc, nname, aof, aon, aoo \ struct.unpack_from(IIIIIII, data, exp_off 12) aof rva_to_offset(aof, sections) # 函数地址表 aon rva_to_offset(aon, sections) # 名字表 aoo rva_to_offset(aoo, sections) # 名字序号表 rows [] for i in range(nname): name_rva struct.unpack_from(I, data, aon i * 4)[0] idx struct.unpack_from(H, data, aoo i * 2)[0] fn_rva struct.unpack_from(I, data, aof idx * 4)[0] name read_cstr(data, rva_to_offset(name_rva, sections)) # 函数 RVA 落在导出目录范围内 转发字符串 if exp_rva fn_rva exp_rva exp_size: target read_cstr(data, rva_to_offset(fn_rva, sections)) rows.append((base idx, name, FORWARD, target)) else: rows.append((base idx, name, f0x{fn_rva:08X}, )) return rows if __name__ __main__: for ordinal, name, addr, target in dump_exports(sys.argv[1]): print(f{ordinal:6d} {name:36s} {addr:10s} {target})逻辑上分五步定位 NT 头、读 COFF 头拿节区数、读可选头拿数据目录、建立节区映射表、解析导出目录三张表。几个参数值得单独说清楚。IIIIIII表示七个 4 字节小端无符号整数正好对应Name、Base、NumberOfFunctions、NumberOfNames、AddressOfFunctions、AddressOfNames、AddressOfNameOrdinals从目录偏移 12 开始读前 12 字节被跳过。rva_to_offset里取max(vsize, raw_size)是因为有些节区文件对齐后的实际字节数比虚拟大小更大只按虚拟大小判断会漏掉尾部。序号计算用Base idx而不是idxWindows 的导出基数通常是 1直接用下标去调GetProcAddress会整体偏移一位。跑完之后可以顺手做个统计把FORWARD的行单独过滤出来看目标模块分布会发现大量函数指向同一两个下层模块。这不是异常而是系统为了保持向后兼容做的分层。理解这一点后面排查“函数明明在却崩在别处”时就不会绕远路。2.3 从名字后缀读出调用约定、字符集与转发标记导出名不是随便起的后缀里带着可以直接用于决策的信息识别出来能省掉一轮试错。最常见的W和A后缀代表宽字符和 ANSI 两个版本CreateFileW收 UTF-16CreateFileA走当前代码页。只要路径里可能出现非 ASCII 字符就必须走W版本否则会拿到一串问号甚至直接失败。ctypes里对应wintypes.LPCWSTR和wintypes.LPCSTR混用会静默传错指针。Ex后缀通常表示扩展版本参数更多、能力更强也更容易在旧系统上缺失比如GetVersionEx系列在较新系统上已经不再推荐使用返回的版本号会被兼容清单影响。带数字后缀的如GetTickCount与GetTickCount64差的不只是返回值宽度还有可用性基线。Ptr后缀和S后缀在 kernel32 里相对少见但在跨模块调用里很关键它们表示同一语义在 32 位和 64 位下的不同入口。调用约定方面x86 上 kernel32 的导出函数是__stdcall对应ctypes.WinDLLCDLL走的是cdecl在 x86 上会导致栈失衡现象是调用返回后进程直接退出且没有明显报错。x64 只有一种调用约定混用不会立刻炸这也让很多问题在 32 位环境下才暴露成了一类典型“换台机器就翻车”的问题。3. 用 ctypes 把 kernel32 调通从最小可跑到参数对齐Python 调 Windows 原生接口ctypes是最省事的路径但它默认不做任何类型检查argtypes和restype不写就等于把参数当整数硬塞。真正稳定的调用需要三件事结构体布局与 C 完全一致、每个函数的签名显式声明、错误码从正确的通道取。下面按从简单到复杂的顺序把这三件事落地。3.1 第一个能跑的调用GetSystemInfo 与结构体对齐GetSystemInfo只收一个输出指针没有返回值适合作为第一个验证目标。结构体必须逐字段还原特别注意 C 里那个匿名联合体dwOemId和{wProcessorArchitecture, wReserved}共用前 4 字节。用两个 WORD 展开是最稳的写法因为联合体在 Python 侧没法直接表达。import ctypes from ctypes import wintypes ARCH {0: x86, 5: ARM, 9: x64, 12: ARM64} class SYSTEM_INFO(ctypes.Structure): _fields_ [ (wProcessorArchitecture, wintypes.WORD), # 联合体前半 (wReserved, wintypes.WORD), # 联合体后半凑满 4 字节 (dwPageSize, wintypes.DWORD), (lpMinimumApplicationAddress, ctypes.c_void_p), (lpMaximumApplicationAddress, ctypes.c_void_p), (dwActiveProcessorMask, ctypes.c_size_t), # 指针宽度随位数变化 (dwNumberOfProcessors, wintypes.DWORD), (dwProcessorType, wintypes.DWORD), (dwAllocationGranularity, wintypes.DWORD), (wProcessorLevel, wintypes.WORD), (wProcessorRevision, wintypes.WORD), ] k32 ctypes.WinDLL(kernel32, use_last_errorTrue) k32.GetSystemInfo.argtypes [ctypes.POINTER(SYSTEM_INFO)] k32.GetSystemInfo.restype None si SYSTEM_INFO() k32.GetSystemInfo(ctypes.byref(si)) print(arch :, ARCH.get(si.wProcessorArchitecture, unknown)) print(page size :, si.dwPageSize) print(alloc gran :, si.dwAllocationGranularity) print(cpu count :, si.dwNumberOfProcessors) print(struct size :, ctypes.sizeof(SYSTEM_INFO)) # x64 上应为 48ctypes.WinDLL而不是ctypes.CDLL是因为 x86 下需要 stdcall 的栈清理约定。use_last_errorTrue让 ctypes 在每次调用后立刻缓存错误码避免后续 Python 操作把它覆盖掉取的时候用ctypes.get_last_error()而不是再调一次GetLastError。c_size_t用在dwActiveProcessorMask上是因为这个字段在 32 位系统里是 32 位、64 位系统里是 64 位写死DWORD会让后面所有字段错位。最后打印结构体大小是一个低成本的自我检查x64 上不是 48 就说明布局有问题后面读出来的值都不可信。3.2 句柄类调用CreateFileW 的七个参数和一次错误码回读句柄类接口是最容易踩坑的一类因为失败时返回的不是False而是INVALID_HANDLE_VALUE而这个值在 64 位下是0xFFFFFFFFFFFFFFFF直接和-1比较会得到错误结论。下面的例子打开一个文件、读前 512 字节、再关掉把完整生命周期走一遍。GENERIC_READ 0x80000000 FILE_SHARE_READ 0x00000001 OPEN_EXISTING 3 FILE_ATTRIBUTE_NORMAL 0x0080 INVALID_HANDLE_VALUE ctypes.c_void_p(-1).value # 64 位上为全 F k32.CreateFileW.argtypes [ wintypes.LPCWSTR, # lpFileName wintypes.DWORD, # dwDesiredAccess wintypes.DWORD, # dwShareMode ctypes.c_void_p, # lpSecurityAttributes wintypes.DWORD, # dwCreationDisposition wintypes.DWORD, # dwFlagsAndAttributes wintypes.HANDLE, # hTemplateFile ] k32.CreateFileW.restype wintypes.HANDLE k32.ReadFile.argtypes [ wintypes.HANDLE, wintypes.LPVOID, wintypes.DWORD, ctypes.POINTER(wintypes.DWORD), ctypes.c_void_p, ] k32.ReadFile.restype wintypes.BOOL k32.CloseHandle.argtypes [wintypes.HANDLE] k32.CloseHandle.restype wintypes.BOOL def read_head(path, n512): h k32.CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, None, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, None) if h is None or h INVALID_HANDLE_VALUE: raise ctypes.WinError(ctypes.get_last_error()) buf ctypes.create_string_buffer(n) nread wintypes.DWORD(0) try: ok k32.ReadFile(h, buf, n, ctypes.byref(nread), None) if not ok: raise ctypes.WinError(ctypes.get_last_error()) return buf.raw[:nread.value] finally: k32.CloseHandle(h) # 无论成败都要关句柄泄漏在高频调用里很致命 print(read_head(rC:\Windows\System32\kernel32.dll, 64)[:16].hex())七个参数里dwDesiredAccess用0x80000000而不是1因为GENERIC_READ的最高位被置位写成1实际请求的是别的权限。dwShareMode传FILE_SHARE_READ允许多个读者同时打开排查日志文件被占用时这个参数往往是关键。lpSecurityAttributes传None表示默认安全描述符只有需要继承句柄时才需要填。dwCreationDisposition传OPEN_EXISTING表示文件必须已存在少了这个约束会得到一个空文件后面读出来全是零看起来像“API 没生效”。错误码的取法在上一节说过必须经由use_last_error缓存ctypes.WinError拿到数值后会翻译成可读消息排查时比一个裸数字有用得多。finally里关句柄是硬要求短脚本看不出来放到循环里几百次之后就会撞上句柄上限。3.3 缓冲区类调用GlobalMemoryStatusEx 里的 dwLength 陷阱带dwLength字段的结构体在 Windows API 里非常多这类设计是为了版本演进调用方告诉系统自己按哪个版本的结构体来读系统只填这么多字节。GlobalMemoryStatusEx是典型代表漏掉长度赋值会直接返回ERROR_INSUFFICIENT_BUFFER或者读回一堆零。class MEMORYSTATUSEX(ctypes.Structure): _fields_ [ (dwLength, wintypes.DWORD), (dwMemoryLoad, wintypes.DWORD), (ullTotalPhys, ctypes.c_ulonglong), (ullAvailPhys, ctypes.c_ulonglong), (ullTotalPageFile, ctypes.c_ulonglong), (ullAvailPageFile, ctypes.c_ulonglong), (ullTotalVirtual, ctypes.c_ulonglong), (ullAvailVirtual, ctypes.c_ulonglong), (ullAvailExtendedVirtual, ctypes.c_ulonglong), ] k32.GlobalMemoryStatusEx.argtypes [ctypes.POINTER(MEMORYSTATUSEX)] k32.GlobalMemoryStatusEx.restype wintypes.BOOL ms MEMORYSTATUSEX() ms.dwLength ctypes.sizeof(MEMORYSTATUSEX) # 必须先赋值且等于结构体真实大小 if not k32.GlobalMemoryStatusEx(ctypes.byref(ms)): raise ctypes.WinError(ctypes.get_last_error()) GB 1024 ** 3 print(fload : {ms.dwMemoryLoad}%) print(ftotal phy : {ms.ullTotalPhys / GB:.1f} GB) print(favail phy : {ms.ullAvailPhys / GB:.1f} GB) print(fvirtual : {ms.ullTotalVirtual / GB:.1f} GB)这里有两个位置极易出错。dwLength要在调用之前赋值且值必须等于结构体的真实字节数写成一个固定常量比如 64在 32 位和 64 位混跑的环境里会翻车。c_ulonglong的对齐是 8 字节而前面两个 4 字节字段正好凑出 8 字节所以ullTotalPhys从偏移 8 开始和 C 定义一致如果把dwMemoryLoad误写成c_ulonglong整个结构体会错位 4 字节读出来的容量会离谱到不像真实值。返回BOOL的接口必须检查返回值只看结构体内容不看返回码等于放弃了唯一的失败信号。3.4 封装成一个可复用的 k32 模块零散调用写成脚本没问题一旦要在项目里用就得把签名声明集中起来避免每个调用点各写一遍还写得不一样。常见的做法是模块级写一个初始化函数所有argtypes/restype在里面一次性设好调用方只拿句柄。def init_kernel32(): dll ctypes.WinDLL(kernel32, use_last_errorTrue) dll.CreateFileW.argtypes [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p, wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE] dll.CreateFileW.restype wintypes.HANDLE dll.CloseHandle.argtypes [wintypes.HANDLE] dll.CloseHandle.restype wintypes.BOOL dll.GetSystemInfo.argtypes [ctypes.POINTER(SYSTEM_INFO)] dll.GetSystemInfo.restype None dll.GetCurrentProcessId.argtypes [] dll.GetCurrentProcessId.restype wintypes.DWORD return dll def check(ok, where): if not ok: raise ctypes.WinError(ctypes.get_last_error())把WinDLL包在函数里而不是模块顶层是为了让导入这个模块的进程在非 Windows 环境下不会直接失败测试环境里也更好打桩。check统一处理BOOL返回值失败时抛WinError调用点就不用每处重复写错误分支。GetCurrentProcessId这类无参接口argtypes设成空列表能避免 ctypes 在某些版本上把残留参数塞进去。集中声明还有一个隐性收益写错类型的概率大幅下降多人协作时不会出现同一函数两种签名共存的情况。4. kernel32.dll 相关的五类故障排查从现象倒推原因线上碰到的 kernel32 问题绝大多数不是这个 DLL 本身损坏而是调用方与它的约定没对上。下面五类按出现频率排每条都给出可观测的现象、真实原因和定位动作遇到时按顺序套即可。4.1 现象程序一启动就提示找不到入口点现象很直白弹窗写着某个过程入口点无法定位程序连主界面都出不来换成开发机又一切正常。原因是导入表在加载阶段就被解析进程启动时加载器会遍历所有静态导入的名字任何一个在当前系统上不存在加载就整体失败。这类问题常见于把在新系统上编译的产物直接丢到旧系统运行或者依赖了某个只在特定版本才有的函数。解决思路是把静态依赖改成动态获取不在导入表里写死运行时用GetModuleHandleW拿到模块句柄再用GetProcAddress按名字查。查不到就走降级分支而不是让整个进程起不来。定位阶段可以用导出清单脚本对目标系统上的同名模块跑一遍看那个名字到底在不在比反复猜版本号高效。4.2 现象0xc000007b 或 BadImageFormatException现象是进程启动瞬间退出事件日志里是0xc000007b托管环境里抛BadImageFormatException消息通常只提到某个模块加载失败不说是位数问题。原因几乎都是 32 位与 64 位混用32 位宿主进程去加载 64 位模块或者反过来。这条错误码的迷惑性在于它不指向具体文件只告诉你“映像格式不对”。解决动作分两步。先确认宿主进程位数用GetSystemInfo返回的架构字段或者任务管理器的平台列再确认目标模块位数读 PE 可选头的魔数0x10B是 32 位、0x20B是 64 位。两边对不上就是它。工程上更彻底的做法是把构建配置里的平台目标统一尽量不要留AnyCPU这种随宿主漂移的设置尤其是会 P/Invoke 原生模块的项目。4.3 现象提示找不到指定模块但文件明明在现象是加载某个自有模块时报“找不到指定的模块”去目录里看文件确实存在路径也没写错。原因是缺失的不一定是它本身而是它依赖的下一个模块。kernel32 作为基础层很多能力并非自己实现而是转发到下层模块或 API Set 上依赖链上任何一环断了报错都会笼统地指向最外层。解决靠依赖树而不是猜。用依赖查看工具把目标模块的完整导入链展开重点看那些标红的项运行时层面用文件系统监视工具过滤“名称未找到”的结果能直接看到加载器在哪些路径上找过、哪些没找到。定位到具体缺失项之后再判断是运行库没装、还是部署时漏拷了文件。4.4 现象加载到了非系统目录的同名文件现象是同一个程序在不同机器上行为不一致某台机器上某个基础调用返回的结果很奇怪换台机器就正常版本号看起来也一样。原因是加载器按搜索顺序找模块应用程序目录的优先级高于系统目录少数已知模块除外如果程序目录里恰好放了一个同名文件被加载的就是那个而不是系统目录里的原件。解决分两头。部署侧不要往程序目录放与系统模块同名的文件这是最省事的做法代码侧可以在启动早期调用SetDefaultDllDirectories限制搜索范围或者用LoadLibraryExW配合明确的搜索标志按绝对路径加载。排查侧则用进程模块列表确认实际加载路径比对预期路径一眼就能看出来加载的是哪一个。4.5 现象结构体读回来全是零或长度不足现象是接口返回成功但结构体里的字段全是零或者调用直接失败并给出长度不足的错误码。原因通常是结构体定义与系统预期不一致字段类型写错导致偏移错位、dwLength没赋值或赋了个不对的常量、联合体没有正确展开。这类问题不会崩只是安静地给出错误数据是最难靠日志发现的一类。解决靠对照和自检。字段类型逐一比对头文件定义指针宽度相关的字段一律用c_size_t或c_void_p凡是带长度字段的结构体赋值用ctypes.sizeof(结构体)而不是手写常量调用前后各打印一次结构体大小和预期值比对。养成这个习惯之后这类问题基本在第一次运行就会暴露不会拖到线上。5. 让 kernel32 调用跨版本稳住能力探测、导出转发与一张对照表跨版本兼容的核心不是“判断系统是哪个版本”而是“判断这个能力在不在”。按版本号分支的代码在新系统上经常因为兼容清单的存在而拿到不符合预期的版本号分支直接走错。我一般的做法是全部改成能力探测需要某个函数时先查再调查不到就走替代路径。5.1 用 GetProcAddress 探测能力别用系统版本号做判断探测的写法很短拿模块句柄、按名字查地址、判断是否为空三步就够。查到了再按函数指针调用查不到就降级。这样写出来的代码在旧系统上不会启动失败在新系统上也能用上更强的能力。版本号判断只在极少数必须区分行为差异的场景下用而且要配合兼容清单一起考虑否则很容易被“系统说你是什么版本”骗过去。5.2 用导出转发写一个兼容垫片自有模块做重命名或者拆分时老的调用方还在按旧名字链接直接改名会让它们全部加载失败。这时可以在模块的.def文件里写转发把旧名字指向新实现导出表里留下一条转发记录加载器会自动跳过去调用方完全无感。EXPORTS OldApiNameNewModule.NewApiName LegacyInitNewModule.Init注意这只适用于自有模块之间的迁移系统目录里的原件不要动也不要在部署目录放与系统模块同名的文件那是上一章说的加载错位问题的源头。5.3 一张可以直接抄的 API 稳定性对照表用途推荐接口稳定性备注取系统信息GetSystemInfo高结构体布局稳定注意指针宽度字段取内存状态GlobalMemoryStatusEx高必须正确赋值dwLength取单调时间GetTickCount64中旧系统缺失需探测后降级到GetTickCount取进程号GetCurrentProcessId高无参数签名简单文件操作CreateFileW/ReadFile高一律用 W 版本句柄必须关动态取地址GetProcAddress高配合GetModuleHandleW使用取版本信息GetVersionExW低受兼容清单影响不建议作为分支依据表格里“稳定性”一列说的是跨版本可用性和行为一致性不是调用难度。标“低”的项不是不能用而是不能拿来当唯一依据。我踩过最深的一个坑是把dwLength写成了固定常量 64在 32 位环境里跑了大半年都没事换到 64 位测试机上内存数字全成了零查了整整一个下午才发现是结构体大小对不上。从那以后凡是带长度字段的结构体我一律用sizeof赋值并且在封装层加一次大小断言。写 Windows 原生调用这件事玄学不多血泪经验基本都集中在“类型宽度、对齐、长度字段”这三件事上把这三样守住剩下的都是查文档能解决的。希望帮到你。本文还有配套的精品资源点击获取