想象你买回一大盒乐高积木,说明书上写着“618片,适合6-99岁”。打开盒子,里面是一张总图纸:先确认这块积木该装在哪,然后按页码翻到对应页,照着图纸一块块拼起来。Windows系统加载和运行程序的时候,面对的也是一份“乐高说明书”——PE文件。PE(Portable Executable)是Windows可执行文件的标准格式,它从DOS时代的MZ格式进化而来,又基于Unix的COFF通用对象格式做了扩展。整份说明书有封面(DOS头)、目录(PE头)、页码索引(节表)、零件清单(导入表)和地址簿(重定位表)。今天,我们就用这份乐高说明书当地图,把PE文件从头到尾拆开看个明白。
掌握PE/COFF格式,是AI安全分析师、系统工具开发者的基本功——它能帮你读懂恶意软件的行为、手工修复崩溃的二进制文件,是“反汇编与系统工具”栈上最硬核的一块地基。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
PE文件总览
翻开乐高说明书的第一页,你首先看到的是产品名称和型号——这是“封面”。PE文件的封面叫DOS头,它的最前面有两个字节的魔数(Magic Number):"MZ",这是DOS时代创始人Mark Zbikowski的缩写。任何一个真正的PE文件,文件开头的这两位都必须是4D 5A。如果打开一个文件发现前两字节不是MZ,那它八成不是Windows可执行文件,就像说明书封面印错了书名,后面内容再精致也没用。
DOS头最重要的字段在文件偏移0x3C处,叫e_lfanew。它是一个4字节的小端整数,存的是“PE头在文件里的位置”。为什么需要它?因为DOS头后面还跟着一段DOS存根(DOS Stub),用于在纯DOS环境下运行程序并输出“此程序无法在DOS模式下运行”的提示。这段存根长度不固定,所以必须靠一个指针精确定位真正的PE头。
拿到e_lfanew指针,就像说明书翻到目录页——接下来,COFF头和可选头这片“总纲”就会逐一展开。有了封面和目录,这份说明书才算真正开始为人所用。
- 确定读取位置从文件开头数到第0x3C这个字节,它是DOS头中的e_lfanew字段。
- 读取4字节用小端字节序读取一个无符号32位整数,得到PE头在文件中的偏移。
- 定位PE头跳转到该偏移处,验证接下来的4字节是否为PE签名
\x50\x45\x00\x00。
COFF与可选头
说明书翻过封面页,接下来是“规格总览”:这个套装适合多大年龄、包含多少片零件、有没有特殊部件。PE头由三部分组成:4字节签名(“PE\0\0“)、20字节COFF头、以及一个长度不定的可选头(Optional Header)。名称虽然叫”可选“,但对可执行文件来说它必须有,就像乐高说明书上说”部分套装含专属人仔”,实际每盒都有。
COFF头里最关键的两个字段:Machine(机器类型)和NumberOfSections(节的数量)。Machine字段决定了这个二进制文件是给哪种CPU用的——0x014C代表x86,0x8664代表x64,0xAA64代表ARM64。如果拿错了,Windows加载器会直接拒绝运行,就像给汽车加错了标号的汽油,发动机就是不转。
可选头里住着几个“大人物”:ImageBase(内存基址,比如exe通常是0x400000,DLL是0x10000000)、AddressOfEntryPoint(程序入口点RVA)、SectionAlignment(内存中的节对齐粒度)和FileAlignment(文件中的对齐粒度)。它们联手决定了程序加载到内存后的“家”在哪里、地方多大。
请记住NumberOfSections这个数——它就像是说明书目录的行数,告诉我们接下来要翻几页。每多一节,就是多一章零件说明。
- 读取Machine字段在COFF头起始处读取2字节,这个值就是CPU架构编号。
- 比较架构值0x014C对应x86平台,0x8664对应x64平台。
- 读取节数量紧跟Machine之后的2字节就是NumberOfSections,它告诉加载器节表有多少行。
节表与节区
说明书翻开目录页,你会看到每一章的名字和起始页码。PE文件的“目录页”叫节表,它紧跟在可选头后面,是一个固定大小为40字节的数组。数组的每一项对应一个节(Section),最常见的节有:.text(代码)、.data(已初始化数据)、.rsrc(资源,比如图标和对话框)、.reloc(重定位)。
节表项的结构像一个“地址翻译卡”:VirtualSize(节在内存中真正占用的字节数)、VirtualAddress(节在内存中的RVA起始地址)、SizeOfRawData(节在文件中占用的字节数)、PointerToRawData(节在文件中的起始偏移)。一套数据,两种视野——文件中的一个节区域,加载到内存后会被映射到一个不同的位置。翻译的桥梁就是RVA。
节和节之间的空隙可不是浪费空间,而是为了对齐。文件中的节按FileAlignment对齐(通常0x200=512字节),内存中的节按SectionAlignment对齐(通常0x1000=4096字节)。就像乐高积木收纳箱里,每个小抽屉的尺寸必须统一,才能整整齐齐地放进大箱子。
节表告诉加载器“这一章从第几页开始、占几页”。但页和页之间可能留白,这就是对齐带来的“呼吸感”——为了性能,多花一点空间也值得。
- 计算对齐需几块原始大小除以FileAlignment,向上取整。
- 乘回对齐粒度把块数乘FileAlignment,得到对齐后的总大小。
数据目录
说明书里除了正文,往往还有一张“索引表”:哪个章节讲了特殊玩法、哪个页面有全尺寸图纸。PE文件的可选头尾部,就藏着这样一张索引——数据目录(Data Directory)。它由16个条目组成,每个条目8字节(4字节RVA + 4字节大小),有点像书架上的16个格子,每个格子贴了标签:0号是导出表、1号是导入表、5号是重定位表、14号是延迟导入表,等等。
数据目录存在的意义在于:让加载器和分析工具不需要扫描整个文件,就能快速“按图索骥”找到关键组件。比如安全分析工具想查看程序导入了哪些函数,直接跳到数据目录第1项,拿到导入表的RVA,然后翻译成文件偏移去读取。这个过程就像你在乐高说明书索引页找到“城堡塔楼拼法在第42页”——不用一页页翻。
内存中的真实地址就是“基址+偏移”的组合。ImageBase是进程地址空间中模块的起始地址,RVA是相对这个基址的偏移,两者一加,就是CPU看得见的绝对虚拟地址(VA)。
有了这张索引表,所有“特殊零件”的位置一目了然。但索引只给了位置,真正的内容还得去对应章节里细读——接下来我们翻开最厚的两章:导入表和导出表。
- 基址加偏移把ImageBase这个“街道号”和RVA这个“门牌号”相加。
- 得到绝对地址结果VA就是进程虚拟地址空间中,该数据真正所在的地址。
导入表机制
一套乐高城堡套装,说明书可能会写上“需另购小骑士人仔”。程序也一样,一个exe往往需要调用其他DLL里的函数,比如MessageBoxW来自user32.dll。这个“需另购”清单就在导入表(Import Table)里。PE加载器启动程序前,会先读导入表,把需要用到的DLL加载进内存,然后把函数地址填进一个叫IAT(Import Address Table)的表格里。
导入表的结构是一组IMAGE_IMPORT_DESCRIPTOR的数组,每个描述符对应一个被导入的DLL。描述符里有三个关键字段:OriginalFirstThunk(指向INT数组的RVA)、FirstThunk(指向IAT数组的RVA)和Name(DLL名字符串的RVA)。INT(Import Name Table)和IAT在文件里是一一对应的“镜像双胞胎”——INT里存的是函数名字符串的RVA(或序号),IAT一开始也存同样的值,但加载完成后,IAT里的值会被替换成函数的真实地址。
为什么要有两份?因为编译器可以用INT做校验、用IAT做跳板。程序运行时调用导入函数,实际是call [IAT + 偏移]——直接从IAT里取出早已填好的真实地址,一次内存访问,毫无延迟。这就像你提前把游戏中需要的武器都放进快捷栏,战斗时一键切换,不用再去仓库翻箱倒柜。
导入表是“采购清单”,IAT是“收货仓库”——程序运行前,仓库已满,货物齐全,拿起就能用。
- 加载器读导入表在数据目录第1项拿到导入表的RVA,翻译成文件偏移。
- 解析每个DLL描述符拿到Name获得DLL名,拿到FirstThunk定位IAT。
- 填充IAT用GetProcAddress算出每个函数的真实地址并写入IAT。
导出表机制
如果乐高公司允许你把自己的独家零件分享给别的套装用,那他们需要一个“公共零件目录”——这就是导出表(Export Table)。DLL把自己愿意公开的函数列在导出表里,别的程序就能通过这个目录找到函数并使用。导出表的数据结构其实是三张平行表:AddressOfFunctions(RVA地址表)、AddressOfNames(函数名字表)和AddressOfNameOrdinals(序号表)。
查找一个导出函数的流程像查字典:先在名字表里找到"MessageBoxW"这个名字在数组中的序号(比如3),然后从序号表第3项读出真正的导出序号(比如7),最后去地址表第7项拿到函数入口RVA。名字是钥匙,序号是抽屉号,地址是抽屉里的宝物。
还有一种更快的查找方式——按序号直接导出。调用方不提供名字,直接用序号去寻址。少了名字查表这一步,速度更快但可读性变差。恶意软件常这么做来增加分析难度,就像藏在工具箱暗格里的特殊零件,不按编号根本找不到。
导出表像一份“共享零件租借中心”的货架:名字、序号、地址三栏齐备,你按着编号取货,DLL就能把函数“借”给你用。
- 拿到序号在序号表中查找名字对应的导出序号。
- 查地址表用序号定位地址表数组的元素。
- 读出函数RVA该位置的4字节就是函数入口的RVA。
重定位机制
假设乐高说明书里有一处写着“把红色砖块放在第10排第3列”,但如果地基尺寸变动,这个坐标就得重算。PE文件也有类似问题:PE头里的ImageBase只是“理想基址”,但当多个DLL同时加载,或者开了ASLR(地址空间随机化),实际的加载基址往往会偏离理想值。程序里的绝对地址就全部“失联”了。重定位表(Base Relocation Table)就是这本“地址簿”,专门记录哪些地址需要在加载时被重新修正。
重定位表以“页”为单位组织:每个块描述一页(4KB)内的所有需要修正的位置。块的头部有两个字段:Page RVA(这一页的起始RVA)和Block Size(块总大小)。后面跟着一堆2字节的条目,每个条目的低12位表示页内偏移,高4位表示重定位类型(取值为3表示HIGHLOW,即需按32位修正)。
加载器计算实际修正时,先用实际加载地址减去理想基址,得到偏移量delta,然后对每个条目指向的内存位置执行“原值加delta”的操作。这样,所有绝对地址都跟着新家重新“落户”。
重定位表这张“搬家地址簿”,保证了程序无论被安排在哪个地址都能正常运行——就像搬家后所有快递自动改了收件人地址,一件不落。
- 计算delta实际基址减去ImageBase,得到搬迁偏移量。
- 定位修正位置每个条目的高4位是类型,低12位是页内偏移。
- 修正绝对值把该位置的原值加上delta。
RVA转文件偏移
乐高说明书目录告诉你在第42页找图纸,但“第42页”指的是书页上的印刷编号,不是PDF文件的物理字节位置。PE文件里同样存在两种坐标系统:RVA(相对虚拟地址,内存视角)和文件偏移(磁盘视角)。程序在磁盘上是一堆字节,加载到内存后又是另一番布局。节在磁盘上的位置和内存中的位置并不同步,所以转换是刚需。
转换算法其实非常简单:拿到一个RVA,遍历节表,找到它落在哪个节的范围里——条件是RVA >= VirtualAddress且RVA < VirtualAddress + VirtualSize。确认归属后,用RVA减去节的VirtualAddress得到节内偏移,再加上节在文件中的PointerToRawData,就得到了文件偏移。这就像先确定“第42页属于城堡章节的第5页”,然后“城堡章节从第100页印刷开始”,所以差值是42-37=5,文件页=100+5=105。
这个转换在PE分析工具、调试器、杀毒引擎里被高频调用。无论你是想读取导入表、解析资源,还是修补代码,第一步永远是RVA转文件偏移。搞不懂这个,就像拿着地图却不知道比例尺——坐标全对,但永远找不到地方。
RVA转文件偏移是连接两个世界的翻译官。有了它,你既能在内存视角理解程序,也能在磁盘视角动手修改文件——就像同一本书,既能看章节页码,也能翻实体书页。
- 匹配节区间遍历节表,找到满足条件的节。
- 算节内偏移RVA减去该节起始RVA,得到目标在节内的偏移。
- 加上文件起点把节内偏移加到PointerToRawData上,得到文件偏移。
AI 应用场景
恶意软件样本分析
阶段:静态特征提取
解决的问题:安全分析师拿到一个未知的exe文件,传统方式是人工用十六进制编辑器慢慢翻查,效率极低且容易遗漏关键特征。AI系统接入PE解析器后,可在毫秒级提取出机器类型、节表、导入导出表等结构化特征,作为恶意分类模型的输入。
具体案例:在某次钓鱼邮件攻击调查中,安全团队收到一个伪装成PDF的exe。AI解析器提取该文件的节表后发现.text节大小异常、隐藏了第三个无名字节区,并且导入表里只有LoadLibrary和GetProcAddress这两个典型的“动态解析”函数——三分钟内判定为高度可疑的加载器样本。
DLL劫持漏洞挖掘
阶段:动态行为建模
解决的问题:传统DLL劫持检测靠人工审计导出表和加载路径,工作量大且容易遗漏。AI系统通过解析PE导出表,批量识别目标程序调用的DLL列表,再结合进程监控日志判断是否存在“不在系统目录、可被替换”的可劫持DLL。
具体案例:某安全团队利用PE导出表解析工具,对一款办公软件的全部模块构建了DLL依赖图谱,发现其插件的加载逻辑缺少绝对路径校验。攻击者只需在同目录放置同名恶意DLL,就能实现劫持。AI系统将此线索自动生成预警,人工复核后确认并修复。
固件与补丁逆向分析
阶段:代码重构
解决的问题:在漏洞修复验证中,需要对比补丁前后两个版本的二进制差异。AI系统利用RVA与文件偏移的自动转换,将补丁前后的函数映射到统一坐标系,自动定位被修改的代码区域,大幅提升逆向分析效率。
具体案例:某CVE漏洞的补丁分析中,AI系统对比了补丁前后的PE节表与函数位置,自动锁定一个位于.text节中的4字节改动,并反向推导该改动重写了某数组的长度检查逻辑。分析人员据此快速写出漏洞利用检测规则。
Python 演示
import structimport sys class PEParser: def __init__(self, filepath): with open(filepath, 'rb') as f: self.data = f.read() self.sections = [] self.parse_dos_and_pe() def read(self, offset, fmt): size = struct.calcsize(fmt) return struct.unpack_from(fmt, self.data, offset) def parse_dos_and_pe(self): if self.data[:2] != b'MZ': raise ValueError('Not a valid PE file: missing MZ') self.e_lfanew = self.read(0x3C, '<I')[0] pe_sig = self.read(self.e_lfanew, '<I')[0] if pe_sig != 0x00004550: raise ValueError('Not a valid PE file: bad PE signature') self.machine, self.num_sections = self.read(self.e_lfanew + 4, '<HH') self.opt_offset = self.e_lfanew + 4 + 20 self.image_base = self.read(self.opt_offset + 28, '<I')[0] self.section_align = self.read(self.opt_offset + 32, '<I')[0] self.file_align = self.read(self.opt_offset + 36, '<I')[0] self.entry_point = self.read(self.opt_offset + 16, '<I')[0] size_of_opt = self.read(self.e_lfanew + 20, '<H')[0] self.section_offset = self.opt_offset + size_of_opt self._parse_sections() def _parse_sections(self): for i in range(self.num_sections): off = self.section_offset + i * 40 name = self.data[off:off + 8].rstrip(b'\x00') vsize = self.read(off + 8, '<I')[0] vaddr = self.read(off + 12, '<I')[0] rawsize = self.read(off + 16, '<I')[0] rawptr = self.read(off + 20, '<I')[0] self.sections.append({'name': name, 'virtual_size': vsize, 'virtual_addr': vaddr, 'raw_size': rawsize, 'raw_ptr': rawptr}) def rva_to_offset(self, rva): for sec in self.sections: start = sec['virtual_addr'] end = start + sec['virtual_size'] if start <= rva < end: return sec['raw_ptr'] + (rva - start) return None def summary(self): print(f'Machine: 0x{self.machine:04X}') print(f'Sections: {self.num_sections}') print(f'ImageBase: 0x{self.image_base:X}') print(f'FileAlign: {self.file_align}') print(f'EntryRVA: 0x{self.entry_point:X}') for sec in self.sections: print(f' {sec["name"].decode("ascii", "ignore"):8s} ' f'VA={sec["virtual_addr"]:06X} ' f'FileOff={sec["raw_ptr"]:06X}') if __name__ == '__main__': path = sys.argv[1] if len(sys.argv) > 1 else 'C:/Windows/System32/notepad.exe' pe = PEParser(path) pe.summary() test_rva = pe.sections[0]['virtual_addr'] + 1 print(f'Test RVA 0x{test_rva:X} -> file offset ' f'0x{pe.rva_to_offset(test_rva):X}')
if self.data[:2] != b'MZ'检查文件头两字节是否为"MZ",这是PE格式的入场券。self.e_lfanew = self.read(0x3C, '<I')[0]从偏移0x3C处读取4字节小端整数,拿到PE头的文件位置。pe_sig != 0x00004550验证PE签名的四个字节"\x50\x45\x00\x00",确保这是真正的PE文件。self.read(self.e_lfanew + 4, '<HH')在PE头偏移4处读取Machine(2字节)和节数量(2字节)。self.image_base = self.read(self.opt_offset + 28, '<I')[0]从可选头第28字节处读取ImageBase,这是模块加载进内存的理想基址。self.section_offset = self.opt_offset + size_of_opt节表的起始位置,等于可选头偏移加上可选头大小。for i in range(self.num_sections)用节数量循环遍历整个节表数组。if start <= rva < end核心匹配逻辑:判断目标RVA是否落在当前节的虚拟地址区间内。return sec['raw_ptr'] + (rva - start)RVA转文件偏移的关键一步:节的文件起始偏移加上节内偏移量。
常见误区
误区一个RVA等于0x1000,又看到文件偏移0x1000处有数据,就以为两者直接相等。有时候我直接拿RVA去文件里找数据,结果读出来的内容完全对不上。
正解RVA是内存视角的地址,文件偏移是磁盘视角的位置。两者之间隔着“节对齐”的差异:磁盘上节按FileAlignment对齐,内存里按SectionAlignment对齐。同一个数据,在磁盘上的位置和内存中的位置往往完全不同,必须通过节表转换。
为何常见:分析工具(如x64dbg)默认显示内存地址,而十六进制编辑器显示文件偏移。初学者在两边来回切换时,容易忽略转换环节,把两个坐标混为一谈。
误区我看磁盘上`.text`节从文件偏移0x400开始,那么它在内存中的RVA也应该从0x400开始吧?
正解节在内存中的起始地址由节表里的VirtualAddress字段决定,与文件偏移无关。一个节的虚拟地址可能是0x1000,而文件偏移只有0x400。加载器按节表信息把文件内容映射到对应的内存地址,而不是简单“一对一”拷贝。
为何常见:人的直觉天然倾向于“原样复制”的映射方式,但实际上PE加载器是“按条目搬砖”,每个节按自己的目标和尺寸搬运。
误区文件开头不是有"PE"两个字母吗?DOS头的所有字段都是PE格式的一部分,我要解析PE头就从文件开头读。
正解真正的PE头从e_lfanew指向的位置才开始,且以"PE\0\0"4字节签名开头。DOS头里的内容(如MZ签名、DOS存根)是历史遗留的兼容层。如果直接从头解析,你会把faterlapp、endjob这些无意义字段当成PE结构,必然解析失败。
为何常见:很多入门工具教程直接展示“文件头”的十六进制视图,MZ和PE的"Ascii"表示挨得近,初学者容易把它们当成同一块区域。
误区FileAlignment和SectionAlignment不都是对齐吗?那它们应该相等,或者随便取一个就行。
正解两者作用域完全不同:FileAlignment是文件中的对齐粒度(常为0x200),保证磁盘上的节紧凑节省空间;SectionAlignment是内存中的对齐粒度(常为0x1000),匹配内存分页机制提升加载效率。内存页是4KB,所以节在内存中也要按4KB对齐,这样每节恰好占整数个页。
为何常见:从名字上看两者很像,且在很多小文件中两个值巧合地都是0x200(当SectionAlignment=0x1000时),初学者没见过差异案例就默认相同。
误区IAT就是导入表,导入表就是IAT,反正都记录导入函数的名字和地址。
正解导入表是描述符的集合,每个描述符指向INT和IAT两个数组。INT(Import Name Table)在文件和内存中都保存函数名/序号信息;IAT(Import Address Table)在文件中和INT内容一致,但加载完成后会被替换为函数真实地址。程序运行时的call [IAT]使用的是IAT,而INT更多被调试器和安全工具用于还原导入信息。
为何常见:在未加载的文件视图里,INT和IAT的值一模一样,初学者拿到的工具(如DIE、CFF Explorer)默认展示的是整合视图,看不到双数组的存在。
面试题
Q1. PE文件开头的DOS签名是以下哪个?
- A."PE\0\0"
- B."MZ"← 正确答案
- C."\x7FELF"
- D."COFF"
答案:B。PE文件开头固定是“MZ“两个字节,这是DOS头签名,致敬DOS作者Mark Zbikowski。”PE\0\0”是PE头自己的签名,位于e_lfanew指向的位置。\x7FELF是Linux ELF文件的魔数。
Q2. e_lfanew字段的作用是什么?
- A.存储CPU架构信息
- B.指向导入表的地址
- C.记录PE头在文件中的偏移← 正确答案
- D.标识节的数量
答案:C。e_lfanew位于文件偏移0x3C处,是一个4字节的小端整数,记录PE头在文件中的确切偏移。因为DOS存根长度不固定,只有通过该指针才能正确定位PE头。
Q3. 哪种Machine值表示64位x86架构?
- A.0x014C
- B.0x8664← 正确答案
- C.0xAA64
- D.0x1C0
答案:B。0x8664对应x64架构,0x014C对应x86(32位),0xAA64对应ARM64。Machine字段是COFF头第一个字段,加载器据此判断能否运行该二进制文件。
Q4. 一个节在内存中的起始地址字段是?
- A.PointerToRawData
- B.SizeOfRawData
- C.VirtualAddress← 正确答案
- D.VirtualSize
答案:C。VirtualAddress记录节在内存中的RVA起始地址;PointerToRawData记录节在文件中的偏移,两者是“内存视角”和“磁盘视角”的对应关系。SizeOfRawData是文件中的节大小。
Q5. RVA转文件偏移的正确步骤是?
- A.直接用RVA当作文件偏移
- B.找到所在节,RVA减VirtualAddress再加PointerToRawData← 正确答案
- C.将RVA与ImageBase相加
- D.找到所在节,RVA减PointerToRawData
答案:B。先遍历节表找到RVA落在哪个节区间,然后用RVA减去该节VirtualAddress得到节内偏移,再加上PointerToRawData。直接拿RVA当文件偏移是初学者最常见的错误。
Q6. 程序运行时调用导入函数,实际通过哪个表跳转?
- A.INT(Import Name Table)
- B.导出表
- C.IAT(Import Address Table)← 正确答案
- D.重定位表
答案:C。加载器会把导入函数的真实地址填入IAT,程序运行时执行call [IAT]完成调用。INT在文件中和IAT内容相同,但加载后不会被替换真实地址,主要用于名称解析和工具分析。
Q7. 程序入口点的RVA存放在哪?
- A.COFF头的Machine字段
- B.可选头的AddressOfEntryPoint字段← 正确答案
- C.数据目录的第0项
- D.节表的VirtualSize字段
答案:B。可选头(Optional Header)中的AddressOfEntryPoint字段记录程序入口函数的RVA。操作系统加载PE后,会跳转到ImageBase + AddressOfEntryPoint处开始执行程序。
Q8. 数据目录中可能包含以下哪些表?
- A.导出表← 正确答案
- B.导入表← 正确答案
- C.重定位表← 正确答案
- D.节表
答案:ABC。数据目录是可选头尾部的16条目索引,其中0号条目是导出表、1号是导入表、5号是重定位表。节表不属于数据目录,它位于可选头之后,是独立存在的数组,记录各节的位置和大小。
Q9. 关于重定位表的描述,哪些是正确的?
- A.它以页为单位组织块← 正确答案
- B.记录ImageBase的值
- C.每个条目高4位是类型← 正确答案
- D.条目低12位表示页内偏移← 正确答案
答案:ACD。重定位表按4KB页分组,块头部有PageRVA和BlockSize,每个2字节条目高4位表示重定位类型(3表示32位修正),低12位是页内偏移。它修正的是绝对地址,与ImageBase字段无关。
Q10. 若DLL实际加载基址与ImageBase不同,以下说法正确的是?
- A.程序中所有绝对地址都会失效← 正确答案
- B.需要重定位表修正绝对地址← 正确答案
- C.delta等于实际基址减ImageBase← 正确答案
- D.修正时直接用delta乘以4
答案:ABC。当ASLR或DLL冲突导致实际基址偏离ImageBase时,所有绝对地址都需要修正。加载器计算delta=实际基址-ImageBase,然后将每个需重定位位置的原值加上delta。修正直接加delta,不需要乘以4。