[{"content":"ZIP 文件结构与解析流程核心笔记 ZIP文件基础结构图示 具体的结构和解析流程在后面会有详细介绍。\nZIP文件主要由三部分构成:\nLocal File Header: 每个压缩文件的起始位置，包含文件的元数据信息； Central Directory(CD): 压缩文件的目录，记录了所有文件的元数据信息； End of Central Directory(EOCDR): 压缩文件的结束位置，包含目录的元数据信息。 只考虑非分卷压缩的情况，EOCDR中包含指向Central Directory的偏移量，Central Directory的大小，以及CD条目的项数。\n中央目录区包含了所有文件的元数据信息，每个文件的元数据信息由一个Central Directory Entry(CD Entry)表示。每个CD项中含有指向对应Local File Header的偏移量。\n本地文件头区包含了每个压缩文件的元数据信息，每个文件的元数据信息由一个Local File Header(LFH)表示。文件的实际数据(压缩或未压缩)紧跟在其对应的LFH后面。\nZIP文件解析流程 本地文件头区已经包含了每个压缩文件的元数据信息，理论上仅通过读取LFH就能解压出压缩包内的所有文件，因此在解析ZIP文件时就产生了两种解析方式。\n标准解析模式: 先读取整个Central Directory，然后根据CD项中的偏移量，随机访问到对应文件的LFH，从而获取文件的元数据信息。 流式解析模式: 从Local File Header开始，顺序解析每个压缩文件的元数据信息，直到无法读取有效的LFH签名为止。 目前大多数ZIP解析器默认采用标准解析模式，忽略掉没有被CD条目指向的LFH。\n标准解析模式 从EOCDR中获取指向Central Directory的偏移量，然后根据CD项中的偏移量，随机访问到对应文件的LFH，从而获取文件的元数据信息。\nsequenceDiagram participant Parser as 解析器 participant File as ZIP文件 participant LFH as 本地文件头 participant CD as 中央目录 participant EOCDR as 中央目录结束记录 Parser-\u003e\u003eFile: 从文件末尾查找EOCDR签名 File--\u003e\u003eParser: 返回EOCDR位置 Parser-\u003e\u003eEOCDR: 读取EOCDR内容 EOCDR--\u003e\u003eParser: 返回EOCDR数据 Parser-\u003e\u003eParser: 提取Central Directory偏移量 Parser-\u003e\u003eFile: 跳转到Central Directory位置 loop 读取所有CD项 Parser-\u003e\u003eCD: 读取一个CentralDirectoryHeader CD--\u003e\u003eParser: 返回CD项数据 Parser-\u003e\u003eParser: 保存CD项 end loop 根据CD项读取所有LFH Parser-\u003e\u003eCD: 从CD项获取Local File Header偏移量 CD--\u003e\u003eParser: 返回偏移量 Parser-\u003e\u003eFile: 跳转到对应的Local File Header位置 Parser-\u003e\u003eLFH: 读取LocalFileHeader LFH--\u003e\u003eParser: 返回LFH数据 Parser-\u003e\u003eParser: 保存LFH end sequenceDiagram participant Parser as 解析器 participant File as ZIP文件 participant LFH as 本地文件头 participant CD as 中央目录 participant EOCDR as 中央目录结束记录 Parser-\u003e\u003eFile: 从文件末尾查找EOCDR签名 File--\u003e\u003eParser: 返回EOCDR位置 Parser-\u003e\u003eEOCDR: 读取EOCDR内容 EOCDR--\u003e\u003eParser: 返回EOCDR数据 Parser-\u003e\u003eParser: 提取Central Directory偏移量 Parser-\u003e\u003eFile: 跳转到Central Directory位置 loop 读取所有CD项 Parser-\u003e\u003eCD: 读取一个CentralDirectoryHeader CD--\u003e\u003eParser: 返回CD项数据 Parser-\u003e\u003eParser: 保存CD项 end loop 根据CD项读取所有LFH Parser-\u003e\u003eCD: 从CD项获取Local File Header偏移量 CD--\u003e\u003eParser: 返回偏移量 Parser-\u003e\u003eFile: 跳转到对应的Local File Header位置 Parser-\u003e\u003eLFH: 读取LocalFileHeader LFH--\u003e\u003eParser: 返回LFH数据 Parser-\u003e\u003eParser: 保存LFH end 流式解析模式 从Local File Header开始，顺序解析每个压缩文件的元数据信息，直到无法读取有效的LFH签名为止。\nsequenceDiagram participant Parser as 解析器 participant File as ZIP文件 participant LFH as 本地文件头 Parser-\u003e\u003eFile: 将文件指针定位到文件开头 loop 顺序读取Local File Header Parser-\u003e\u003eLFH: 尝试读取LocalFileHeader alt 成功读取LFH签名 LFH--\u003e\u003eParser: 返回LocalFileHeader数据 Parser-\u003e\u003eParser: 保存LFH Parser-\u003e\u003eFile: 继续读取下一个LFH else 未找到有效的LFH签名或读取失败 File--\u003e\u003eParser: 返回读取失败 Parser-\u003e\u003eParser: 停止解析 end end sequenceDiagram participant Parser as 解析器 participant File as ZIP文件 participant LFH as 本地文件头 Parser-\u003e\u003eFile: 将文件指针定位到文件开头 loop 顺序读取Local File Header Parser-\u003e\u003eLFH: 尝试读取LocalFileHeader alt 成功读取LFH签名 LFH--\u003e\u003eParser: 返回LocalFileHeader数据 Parser-\u003e\u003eParser: 保存LFH Parser-\u003e\u003eFile: 继续读取下一个LFH else 未找到有效的LFH签名或读取失败 File--\u003e\u003eParser: 返回读取失败 Parser-\u003e\u003eParser: 停止解析 end end 手工篡改ZIP进行实验 zip_analyze目录为一个Zip解析器实例，可以使用两种解析模式读取未加密的Zip文件结构。另外该目录下包含两个Zip文件，分别为zip_demo.zip和zip_demo_hacked.zip。两者的区别在于zip_demo_hacked.zip在zip_demo.zip的基础上，在LFH区添加了一个额外的文件hacked.txt对应的LFH及其数据。具体如下图所示：\n使用zip_analyze解析zip_demo_hacked.zip文件，在标准解析模式下，zip_analyze忽略掉了仅在LFH中存在的hacked.txt文件；而在流式解析模式下，zip_analyze能够成功解析出hacked.txt文件的信息。\nZip文件具体分析 根据官方文档, Zip文件的结构如下:\n[local file header 1] [encryption header 1] [file data 1] [data descriptor 1] . . . [local file header n] [encryption header n] [file data n] [data descriptor n] [archive decryption header] [archive extra data record] [central directory header 1] . . . [central directory header n] [zip64 end of central directory record] [zip64 end of central directory locator] [end of central directory record] 本地文件头 - Local File Header uint32_t signature; uint16_t version_needed; uint16_t general_bit_flag; uint16_t compression_method; uint16_t last_mod_time; uint16_t last_mod_date; uint32_t crc32; uint32_t compressed_size; uint32_t uncompressed_size; uint16_t filename_length; uint16_t extra_field_length; std::string filename; std::unique_ptr\u0026lt;uint8_t[]\u0026gt; extra_field; 本地文件头的extra_field没有固定的内容，不同的压缩器可能会在extra_field中添加不同的信息。但是在extra_field中添加信息时通常要遵循统一的结构: 标签 - 长度 - 数据。\nTag（2 字节）：标识该扩展块的类型（如操作系统相关信息、压缩算法扩展等）。 Length（2 字节）：表示后续数据的长度。 Data：具体的扩展数据，格式由 Tag 定义。 具体标签值和含义的映射可以参考官方文档的4.5.2节，其中列举了由PKWARE定义的标签值和含义。\ngeneral_bit_flag 加密头 - Encryption Header 是否存在: 仅当general_bit_flag的第0位为1时，才存在加密头。\n加密头的具体长度与结构由general_bit_flag的第6位确定：\n当第六位为0时，加密头格式为传统PKWARE加密格式，长度为12字节； 当第六位为1时，加密头格式强加密格式，长度为可变，至少30字节。 IVSize 2 bytes - 初始化向量大小 IVData IVSize - 初始化向量数据 Size 4 bytes - 剩余解密头数据大小 Format 2 bytes - 格式定义 (当前必须为3) AlgID 2 bytes - 加密算法标识符 Bitlen 2 bytes - 密钥长度 (32-448 bits) Flags 2 bytes - 处理标志 ErdSize 2 bytes - 加密随机数据大小 ErdData ErdSize - 加密的随机数据 Reserved1 4 bytes - 证书处理保留字段 Reserved2 (var) - 证书处理保留字段 VSize 2 bytes - 密码验证数据大小 VData VSize-4 - 密码验证数据 (加密) VCRC32 4 bytes - 密码验证数据的CRC32 (加密) 校验密码是否正确即是通过加密头内的部分字段进行的。\n数据描述符 - Data Descriptor 用于在流式压缩场景下，将压缩数据的CRC-32、压缩大小和未压缩大小等信息从文件数据中分离出来。\n由于lfh在文件数据之前，因此在流式压缩场景下，无法提前确定lfh中的crc32, compressed_size和uncompressed_size字段。此时，需要在文件数据之后添加一个数据描述符，用于存储这些信息。\nuint32_t signature; /* 0x08074b50 */ uint32_t crc32; uint32_t compressed_size; uint32_t uncompressed_size; 归档解密头 - Archive Decryption Header 结构与加密头相同，不同的是加密头是在每个文件数据之前，而归档解密头是在所有文件数据之后。归档解密头的位置由Zip64 End of Central Directory Record中的Start of Central Directory字段指定。\n使用归档解密头可以支持加密整个中央目录结构，保护所有文件的元数据。\n只在Zip64格式下才会存在归档解密头。\n归档解密头的结构与Encryption Header完全相同。\n归档额外数据记录 - Archive Extra Data Record Archive Extra Data Record 主要用于存储与中央目录加密相关的额外信息，特别是：\n数字证书信息：存储 PKCS#7 证书存储、X.509 证书 ID 和签名等 加密相关数据：存储加密接收者证书列表等 其结构定义如下：\nuint32_t signature; /* 0x08074b50 */ uint32_t extra_field_length; std::unique_ptr\u0026lt;uint8_t[]\u0026gt; extra_field; 中央目录头 - Central Directory Header uint32_t signature; uint16_t version_made_by; uint16_t version_needed; uint16_t general_bit_flag; uint16_t compression_method; uint16_t last_mod_time; uint16_t last_mod_date; uint32_t crc32; uint32_t compressed_size; uint32_t uncompressed_size; uint16_t filename_length; uint16_t extra_field_length; uint16_t file_comment_length; uint16_t disk_number_start; uint16_t internal_attr; uint32_t external_attr; uint32_t local_header_offset; std::string filename; std::unique_ptr\u0026lt;uint8_t[]\u0026gt; extra_field; std::string file_comment; 中央目录头与本地文件头之间存在许多冗余字段，原则上相对应的一对中央目录头和本地文件头，表示含义相同的字段值应该是相同的。\n相关流程 Zip64解析流程 sequenceDiagram participant Parser AS 解析器 participant EOCDR AS End of Central Directory participant ZIP64Locator AS ZIP64 Locator participant ZIP64EOCDR AS ZIP64 End of Central Directory participant 中央目录 AS Central Directory participant CDH AS Central Directory Header participant LFH AS Local File Header participant 文件数据 AS File Data participant DD AS Data Descriptor Note over Parser: 初始化阶段 Parser-\u003e\u003eEOCDR: 1. 读取End of Central Directory Record EOCDR-\u003e\u003eParser: 返回EOCDR数据 Parser-\u003e\u003eParser: 2. 检查ZIP64格式标志 alt 检测到ZIP64标志 Parser-\u003e\u003eZIP64Locator: 3. 读取ZIP64 Locator ZIP64Locator-\u003e\u003eParser: 返回ZIP64 EOCDR位置 Parser-\u003e\u003eZIP64EOCDR: 4. 读取ZIP64 EOCDR ZIP64EOCDR-\u003e\u003eParser: 返回完整中央目录信息 end Parser-\u003e\u003e中央目录: 5. 定位中央目录 Parser-\u003e\u003eParser: 6. 初始化文件计数器 = 0 Note over Parser: 循环处理所有文件 loop 对于每个文件条目 Parser-\u003e\u003eParser: 7. 文件计数器 += 1 Parser-\u003e\u003eCDH: 8. 读取Central Directory Header CDH-\u003e\u003eParser: 返回文件元数据 Parser-\u003e\u003eParser: 9. 检查是否需要ZIP64扩展 alt 需要ZIP64扩展 Parser-\u003e\u003eCDH: 10. 提取ZIP64扩展信息 CDH-\u003e\u003eParser: 返回8字节大小和偏移量 end Note over Parser: 处理Local File Header Parser-\u003e\u003eLFH: 11. 根据CDH定位LFH LFH-\u003e\u003eParser: 返回Local File Header数据 Parser-\u003e\u003eParser: 12. 验证LFH与CDH一致性 Note over Parser: 处理文件数据 Parser-\u003e\u003e文件数据: 13. 读取压缩文件数据 文件数据-\u003e\u003eParser: 返回文件数据 Parser-\u003e\u003eParser: 14. 解压/解密文件数据 alt 存在Data Descriptor Parser-\u003e\u003eDD: 15. 读取Data Descriptor DD-\u003e\u003eParser: 返回实际CRC和大小 Parser-\u003e\u003eParser: 16. 验证数据完整性 end Parser-\u003e\u003eParser: 17. 存储文件信息 Parser-\u003e\u003eParser: 18. 检查是否还有更多文件 end Note over Parser: 完成阶段 Parser-\u003e\u003eParser: 19. 生成完整文件列表 Parser-\u003e\u003eParser: 20. 验证所有文件完整性 Parser-\u003e\u003eParser: 21. 返回解析结果 sequenceDiagram participant Parser AS 解析器 participant EOCDR AS End of Central Directory participant ZIP64Locator AS ZIP64 Locator participant ZIP64EOCDR AS ZIP64 End of Central Directory participant 中央目录 AS Central Directory participant CDH AS Central Directory Header participant LFH AS Local File Header participant 文件数据 AS File Data participant DD AS Data Descriptor Note over Parser: 初始化阶段 Parser-\u003e\u003eEOCDR: 1. 读取End of Central Directory Record EOCDR-\u003e\u003eParser: 返回EOCDR数据 Parser-\u003e\u003eParser: 2. 检查ZIP64格式标志 alt 检测到ZIP64标志 Parser-\u003e\u003eZIP64Locator: 3. 读取ZIP64 Locator ZIP64Locator-\u003e\u003eParser: 返回ZIP64 EOCDR位置 Parser-\u003e\u003eZIP64EOCDR: 4. 读取ZIP64 EOCDR ZIP64EOCDR-\u003e\u003eParser: 返回完整中央目录信息 end Parser-\u003e\u003e中央目录: 5. 定位中央目录 Parser-\u003e\u003eParser: 6. 初始化文件计数器 = 0 Note over Parser: 循环处理所有文件 loop 对于每个文件条目 Parser-\u003e\u003eParser: 7. 文件计数器 += 1 Parser-\u003e\u003eCDH: 8. 读取Central Directory Header CDH-\u003e\u003eParser: 返回文件元数据 Parser-\u003e\u003eParser: 9. 检查是否需要ZIP64扩展 alt 需要ZIP64扩展 Parser-\u003e\u003eCDH: 10. 提取ZIP64扩展信息 CDH-\u003e\u003eParser: 返回8字节大小和偏移量 end Note over Parser: 处理Local File Header Parser-\u003e\u003eLFH: 11. 根据CDH定位LFH LFH-\u003e\u003eParser: 返回Local File Header数据 Parser-\u003e\u003eParser: 12. 验证LFH与CDH一致性 Note over Parser: 处理文件数据 Parser-\u003e\u003e文件数据: 13. 读取压缩文件数据 文件数据-\u003e\u003eParser: 返回文件数据 Parser-\u003e\u003eParser: 14. 解压/解密文件数据 alt 存在Data Descriptor Parser-\u003e\u003eDD: 15. 读取Data Descriptor DD-\u003e\u003eParser: 返回实际CRC和大小 Parser-\u003e\u003eParser: 16. 验证数据完整性 end Parser-\u003e\u003eParser: 17. 存储文件信息 Parser-\u003e\u003eParser: 18. 检查是否还有更多文件 end Note over Parser: 完成阶段 Parser-\u003e\u003eParser: 19. 生成完整文件列表 Parser-\u003e\u003eParser: 20. 验证所有文件完整性 Parser-\u003e\u003eParser: 21. 返回解析结果 启用中央目录加密时的解析流程 sequenceDiagram participant Parser AS 解析器 participant User AS 用户 participant EOCD AS End of Central Directory participant ZIP64Locator AS ZIP64 Locator participant ZIP64EOCD AS ZIP64 End of Central Directory participant ArchiveDH AS Archive Decryption Header participant ArchiveExtra AS Archive Extra Data Record participant EncryptedCD AS Encrypted Central Directory participant CDH AS Central Directory Header participant LFH AS Local File Header participant FileData AS File Data Note over Parser: 阶段1: 定位和读取目录结构 Parser-\u003e\u003eEOCD: 1. 读取End of Central Directory EOCD-\u003e\u003eParser: 返回EOCD数据 Parser-\u003e\u003eParser: 2. 检测ZIP64格式标志 Parser-\u003e\u003eZIP64Locator: 3. 读取ZIP64 Locator ZIP64Locator-\u003e\u003eParser: 返回ZIP64 EOCD位置 Parser-\u003e\u003eZIP64EOCD: 4. 读取ZIP64 EOCD ZIP64EOCD-\u003e\u003eParser: 返回完整目录信息(含加密标志) Note over Parser: 阶段2: 检测中央目录加密 Parser-\u003e\u003eParser: 5. 检查general purpose bit flag 13 alt 中央目录已加密 Parser-\u003e\u003eUser: 6. 请求密码或证书 User-\u003e\u003eParser: 7. 提供密码/证书 Parser-\u003e\u003eArchiveDH: 8. 读取Archive Decryption Header ArchiveDH-\u003e\u003eParser: 返回加密的解密头 Parser-\u003e\u003eParser: 9. 使用密码/证书解密ArchiveDH ArchiveDH-\u003e\u003eParser: 返回解密后的解密头数据 Note over Parser: 阶段3: 解密中央目录 Parser-\u003e\u003eEncryptedCD: 10. 读取加密的中央目录 EncryptedCD-\u003e\u003eParser: 返回加密的中央目录数据 Parser-\u003e\u003eParser: 11. 使用ArchiveDH中的密钥解密中央目录 EncryptedCD-\u003e\u003eParser: 返回解密后的中央目录 Note over Parser: 阶段4: 处理Archive Extra Data Parser-\u003e\u003eArchiveExtra: 12. 读取Archive Extra Data Record ArchiveExtra-\u003e\u003eParser: 返回额外数据(证书等) else 中央目录未加密 Parser-\u003e\u003eEncryptedCD: 8. 直接读取中央目录 EncryptedCD-\u003e\u003eParser: 返回未加密的中央目录 end Note over Parser: 阶段5: 解析中央目录 Parser-\u003e\u003eCDH: 13. 解析中央目录条目 CDH-\u003e\u003eParser: 返回文件元数据 Parser-\u003e\u003eParser: 14. 检查是否需要ZIP64扩展 Note over Parser: 阶段6: 解压文件数据 loop 对于每个文件 Parser-\u003e\u003eLFH: 15. 根据CDH定位LFH LFH-\u003e\u003eParser: 返回Local File Header Parser-\u003e\u003eParser: 16. 验证LFH(注意掩码字段) Parser-\u003e\u003eFileData: 17. 读取文件数据 FileData-\u003e\u003eParser: 返回压缩/加密的文件数据 Parser-\u003e\u003eParser: 18. 解密/解压文件数据 Parser-\u003e\u003eParser: 19. 验证文件完整性 Parser-\u003e\u003eParser: 20. 输出解压后的文件 end Note over Parser: 阶段7: 完成解压 Parser-\u003e\u003eUser: 21. 显示解压完成信息 sequenceDiagram participant Parser AS 解析器 participant User AS 用户 participant EOCD AS End of Central Directory participant ZIP64Locator AS ZIP64 Locator participant ZIP64EOCD AS ZIP64 End of Central Directory participant ArchiveDH AS Archive Decryption Header participant ArchiveExtra AS Archive Extra Data Record participant EncryptedCD AS Encrypted Central Directory participant CDH AS Central Directory Header participant LFH AS Local File Header participant FileData AS File Data Note over Parser: 阶段1: 定位和读取目录结构 Parser-\u003e\u003eEOCD: 1. 读取End of Central Directory EOCD-\u003e\u003eParser: 返回EOCD数据 Parser-\u003e\u003eParser: 2. 检测ZIP64格式标志 Parser-\u003e\u003eZIP64Locator: 3. 读取ZIP64 Locator ZIP64Locator-\u003e\u003eParser: 返回ZIP64 EOCD位置 Parser-\u003e\u003eZIP64EOCD: 4. 读取ZIP64 EOCD ZIP64EOCD-\u003e\u003eParser: 返回完整目录信息(含加密标志) Note over Parser: 阶段2: 检测中央目录加密 Parser-\u003e\u003eParser: 5. 检查general purpose bit flag 13 alt 中央目录已加密 Parser-\u003e\u003eUser: 6. 请求密码或证书 User-\u003e\u003eParser: 7. 提供密码/证书 Parser-\u003e\u003eArchiveDH: 8. 读取Archive Decryption Header ArchiveDH-\u003e\u003eParser: 返回加密的解密头 Parser-\u003e\u003eParser: 9. 使用密码/证书解密ArchiveDH ArchiveDH-\u003e\u003eParser: 返回解密后的解密头数据 Note over Parser: 阶段3: 解密中央目录 Parser-\u003e\u003eEncryptedCD: 10. 读取加密的中央目录 EncryptedCD-\u003e\u003eParser: 返回加密的中央目录数据 Parser-\u003e\u003eParser: 11. 使用ArchiveDH中的密钥解密中央目录 EncryptedCD-\u003e\u003eParser: 返回解密后的中央目录 Note over Parser: 阶段4: 处理Archive Extra Data Parser-\u003e\u003eArchiveExtra: 12. 读取Archive Extra Data Record ArchiveExtra-\u003e\u003eParser: 返回额外数据(证书等) else 中央目录未加密 Parser-\u003e\u003eEncryptedCD: 8. 直接读取中央目录 EncryptedCD-\u003e\u003eParser: 返回未加密的中央目录 end Note over Parser: 阶段5: 解析中央目录 Parser-\u003e\u003eCDH: 13. 解析中央目录条目 CDH-\u003e\u003eParser: 返回文件元数据 Parser-\u003e\u003eParser: 14. 检查是否需要ZIP64扩展 Note over Parser: 阶段6: 解压文件数据 loop 对于每个文件 Parser-\u003e\u003eLFH: 15. 根据CDH定位LFH LFH-\u003e\u003eParser: 返回Local File Header Parser-\u003e\u003eParser: 16. 验证LFH(注意掩码字段) Parser-\u003e\u003eFileData: 17. 读取文件数据 FileData-\u003e\u003eParser: 返回压缩/加密的文件数据 Parser-\u003e\u003eParser: 18. 解密/解压文件数据 Parser-\u003e\u003eParser: 19. 验证文件完整性 Parser-\u003e\u003eParser: 20. 输出解压后的文件 end Note over Parser: 阶段7: 完成解压 Parser-\u003e\u003eUser: 21. 显示解压完成信息 校验密码是否正确 传统PKWARE加密格式校验密码 sequenceDiagram participant User as 用户 participant Parser as 解析器 participant ZIP as ZIP文件 User-\u003e\u003eParser: 提供密码 Parser-\u003e\u003eZIP: 读取Local File Header ZIP-\u003e\u003eParser: 返回LFH数据 (包含加密标志) Parser-\u003e\u003eZIP: 读取12字节加密头 ZIP-\u003e\u003eParser: 返回加密头数据 Parser-\u003e\u003eParser: 使用密码初始化加密密钥 Parser-\u003e\u003eParser: 解密12字节加密头 Parser-\u003e\u003eZIP: 读取文件CRC值 ZIP-\u003e\u003eParser: 返回文件CRC Parser-\u003e\u003eParser: 提取解密后加密头的最后1/2字节 Parser-\u003e\u003eParser: 比较是否等于文件CRC的高位字节 alt 密码验证成功 Parser-\u003e\u003eUser: 密码验证成功 Parser-\u003e\u003eZIP: 继续读取文件数据 ZIP-\u003e\u003eParser: 返回文件数据 Parser-\u003e\u003eParser: 解密文件数据 Parser-\u003e\u003eUser: 解压完成 else 密码验证失败 Parser-\u003e\u003eUser: 密码验证失败 Parser-\u003e\u003eUser: 提示重新输入密码 end sequenceDiagram participant User as 用户 participant Parser as 解析器 participant ZIP as ZIP文件 User-\u003e\u003eParser: 提供密码 Parser-\u003e\u003eZIP: 读取Local File Header ZIP-\u003e\u003eParser: 返回LFH数据 (包含加密标志) Parser-\u003e\u003eZIP: 读取12字节加密头 ZIP-\u003e\u003eParser: 返回加密头数据 Parser-\u003e\u003eParser: 使用密码初始化加密密钥 Parser-\u003e\u003eParser: 解密12字节加密头 Parser-\u003e\u003eZIP: 读取文件CRC值 ZIP-\u003e\u003eParser: 返回文件CRC Parser-\u003e\u003eParser: 提取解密后加密头的最后1/2字节 Parser-\u003e\u003eParser: 比较是否等于文件CRC的高位字节 alt 密码验证成功 Parser-\u003e\u003eUser: 密码验证成功 Parser-\u003e\u003eZIP: 继续读取文件数据 ZIP-\u003e\u003eParser: 返回文件数据 Parser-\u003e\u003eParser: 解密文件数据 Parser-\u003e\u003eUser: 解压完成 else 密码验证失败 Parser-\u003e\u003eUser: 密码验证失败 Parser-\u003e\u003eUser: 提示重新输入密码 end 强加密 sequenceDiagram participant User as 用户 participant Parser as 解析器 participant ZIP as ZIP文件 User-\u003e\u003eParser: 提供密码 Parser-\u003e\u003eZIP: 读取Local File Header ZIP-\u003e\u003eParser: 返回LFH数据 (包含强加密标志) Parser-\u003e\u003eZIP: 读取强加密解密头 ZIP-\u003e\u003eParser: 返回解密头数据 (IVSize, IVData, AlgID, VSize等) Parser-\u003e\u003eParser: 解析解密头明文字段 Parser-\u003e\u003eParser: 使用密码和IVData生成主会话密钥 Parser-\u003e\u003eZIP: 读取ErdData字段 ZIP-\u003e\u003eParser: 返回ErdData Parser-\u003e\u003eParser: 解密ErdData获取随机数据 Parser-\u003e\u003eParser: 生成文件会话密钥 Parser-\u003e\u003eZIP: 读取VData和VCRC32字段 ZIP-\u003e\u003eParser: 返回VData和VCRC32 Parser-\u003e\u003eParser: 使用会话密钥解密VData和VCRC32 Parser-\u003e\u003eParser: 计算解密后VData的CRC32 Parser-\u003e\u003eParser: 比较计算的CRC32与解密的VCRC32 alt 密码验证成功 Parser-\u003e\u003eUser: 密码验证成功 Parser-\u003e\u003eZIP: 读取文件数据 ZIP-\u003e\u003eParser: 返回文件数据 Parser-\u003e\u003eParser: 解密文件数据 Parser-\u003e\u003eUser: 解压完成 else 密码验证失败 Parser-\u003e\u003eUser: 密码验证失败 Parser-\u003e\u003eUser: 提示重新输入密码 end sequenceDiagram participant User as 用户 participant Parser as 解析器 participant ZIP as ZIP文件 User-\u003e\u003eParser: 提供密码 Parser-\u003e\u003eZIP: 读取Local File Header ZIP-\u003e\u003eParser: 返回LFH数据 (包含强加密标志) Parser-\u003e\u003eZIP: 读取强加密解密头 ZIP-\u003e\u003eParser: 返回解密头数据 (IVSize, IVData, AlgID, VSize等) Parser-\u003e\u003eParser: 解析解密头明文字段 Parser-\u003e\u003eParser: 使用密码和IVData生成主会话密钥 Parser-\u003e\u003eZIP: 读取ErdData字段 ZIP-\u003e\u003eParser: 返回ErdData Parser-\u003e\u003eParser: 解密ErdData获取随机数据 Parser-\u003e\u003eParser: 生成文件会话密钥 Parser-\u003e\u003eZIP: 读取VData和VCRC32字段 ZIP-\u003e\u003eParser: 返回VData和VCRC32 Parser-\u003e\u003eParser: 使用会话密钥解密VData和VCRC32 Parser-\u003e\u003eParser: 计算解密后VData的CRC32 Parser-\u003e\u003eParser: 比较计算的CRC32与解密的VCRC32 alt 密码验证成功 Parser-\u003e\u003eUser: 密码验证成功 Parser-\u003e\u003eZIP: 读取文件数据 ZIP-\u003e\u003eParser: 返回文件数据 Parser-\u003e\u003eParser: 解密文件数据 Parser-\u003e\u003eUser: 解压完成 else 密码验证失败 Parser-\u003e\u003eUser: 密码验证失败 Parser-\u003e\u003eUser: 提示重新输入密码 end 启用中央目录加密 sequenceDiagram participant User as 用户 participant Parser as 解析器 participant ZIP as ZIP文件 Parser-\u003e\u003eZIP: 读取EOCD和ZIP64记录 ZIP-\u003e\u003eParser: 返回记录数据 (包含中央目录加密标志) Parser-\u003e\u003eUser: 检测到中央目录加密，请提供密码 User-\u003e\u003eParser: 提供密码 Parser-\u003e\u003eZIP: 根据ZIP64记录定位Archive Decryption Header ZIP-\u003e\u003eParser: 返回Archive Decryption Header Parser-\u003e\u003eParser: 解析解密头明文字段 Parser-\u003e\u003eParser: 使用密码生成主会话密钥 Parser-\u003e\u003eZIP: 读取ErdData字段 ZIP-\u003e\u003eParser: 返回ErdData Parser-\u003e\u003eParser: 解密ErdData获取随机数据 Parser-\u003e\u003eParser: 生成中央目录解密密钥 Parser-\u003e\u003eZIP: 读取VData和VCRC32字段 ZIP-\u003e\u003eParser: 返回VData和VCRC32 Parser-\u003e\u003eParser: 解密VData和VCRC32 Parser-\u003e\u003eParser: 验证VData的CRC32 alt 密码验证成功 Parser-\u003e\u003eZIP: 读取加密的中央目录 ZIP-\u003e\u003eParser: 返回加密的中央目录数据 Parser-\u003e\u003eParser: 解密中央目录 Parser-\u003e\u003eParser: 解析中央目录获取文件列表 Parser-\u003e\u003eUser: 显示文件列表 User-\u003e\u003eParser: 选择要解压的文件 Parser-\u003e\u003eParser: 继续解压选中的文件 Parser-\u003e\u003eUser: 解压完成 else 密码验证失败 Parser-\u003e\u003eUser: 密码验证失败，无法解密中央目录 Parser-\u003e\u003eUser: 无法显示文件列表，请重新输入密码 end sequenceDiagram participant User as 用户 participant Parser as 解析器 participant ZIP as ZIP文件 Parser-\u003e\u003eZIP: 读取EOCD和ZIP64记录 ZIP-\u003e\u003eParser: 返回记录数据 (包含中央目录加密标志) Parser-\u003e\u003eUser: 检测到中央目录加密，请提供密码 User-\u003e\u003eParser: 提供密码 Parser-\u003e\u003eZIP: 根据ZIP64记录定位Archive Decryption Header ZIP-\u003e\u003eParser: 返回Archive Decryption Header Parser-\u003e\u003eParser: 解析解密头明文字段 Parser-\u003e\u003eParser: 使用密码生成主会话密钥 Parser-\u003e\u003eZIP: 读取ErdData字段 ZIP-\u003e\u003eParser: 返回ErdData Parser-\u003e\u003eParser: 解密ErdData获取随机数据 Parser-\u003e\u003eParser: 生成中央目录解密密钥 Parser-\u003e\u003eZIP: 读取VData和VCRC32字段 ZIP-\u003e\u003eParser: 返回VData和VCRC32 Parser-\u003e\u003eParser: 解密VData和VCRC32 Parser-\u003e\u003eParser: 验证VData的CRC32 alt 密码验证成功 Parser-\u003e\u003eZIP: 读取加密的中央目录 ZIP-\u003e\u003eParser: 返回加密的中央目录数据 Parser-\u003e\u003eParser: 解密中央目录 Parser-\u003e\u003eParser: 解析中央目录获取文件列表 Parser-\u003e\u003eUser: 显示文件列表 User-\u003e\u003eParser: 选择要解压的文件 Parser-\u003e\u003eParser: 继续解压选中的文件 Parser-\u003e\u003eUser: 解压完成 else 密码验证失败 Parser-\u003e\u003eUser: 密码验证失败，无法解密中央目录 Parser-\u003e\u003eUser: 无法显示文件列表，请重新输入密码 end ","date":"2026-04-24T02:10:00+08:00","permalink":"https://xingfend.github.io/blog-by-hugo/post/zip-structure-notes/","title":"ZIP 文件结构与解析流程核心笔记"},{"content":"汇编语言预备知识 指令简介与总览 汇编指令 指令格式 功能 备注 lea lea reg, addr 主要用于将一个内存地址加载到寄存器中，\n而不是将该地址处的数据加载到寄存器。 如在printf函数中，将格式字符串的地址加载到指定寄存器中供printf函数读取。 inc inc reg 寄存器自增1 变量加一 mov mov reg, imm 将立即数赋值到寄存器中 imm意为立即数(immediate number) imul 指令格式见详解 用于执行带符号整数乘法。 它可以接受一个、两个或三个操作数，具体取决于你想要执行的乘法类型。 mul mul source 用于执行无符号整数乘法。 mul 指令只接受一个操作数，这个操作数可以是寄存器或内存地址。根据操作数的大小（8位、16位、32位或64位），乘积会被存储在特定的寄存器组合中。 shl/shr shl/shr dest, cnt 逻辑左/右移 shr用于将寄存器或内存中的二进制数值向右移动指定的位数。这个操作会将每个位都向右移动，并在左侧补0。对于无符号数来说，这相当于除以2的幂次；而对于有符号数，它仅当被移位的数据视为无符号时有效，因为逻辑右移不会复制符号位。 sar sar dest, count 算术左/右移 sar指令用于将指定的二进制数按算术右移的方式移动指定的位数。算术右移是指在右移时，最高位（符号位）保持不变，低位移出，高位补上符号位的值。这通常用于有符号数的除以2的幂次操作。 cdq cdq 将32位寄存器eax中的有符号数符号扩展为64位，结果存储在由edx:eax组成的64位寄存器中 它会把eax的第31位（符号位）复制到edx的每一位。如果eax的符号位是0（即正数），则edx会被设置为0；如果符号位是1（即负数），则edx会被设置为全1。\n通常在执行32位有符号除法（如idiv指令）之前使用，以确保被除数的符号位正确扩展到高位寄存器，从而保证除法运算的正确性。 cqo cqo 将64位寄存器rax中的有符号数符号扩展为128位，结果存储在由rdx:rax组成的128位寄存器中 它会把rax的第63位（符号位）复制到rdx的每一位。如果rax的符号位是0，则rdx被设置为0；如果符号位是1，则rdx被设置为全1。\n在64位模式下，通常在执行64位有符号除法之前使用，确保被除数的符号位正确扩展到高位寄存器。 imul - 带符号整数乘法 一个操作数 imul src ​\t这种形式假设隐含的目标操作数是累加器（对于16位操作为AX，对于32位操作为EAX，对于64位操作为RAX）。该指令将累加器与source相乘，并将结果存储回累加器和其对应的双倍宽度寄存器（即对于16位操作为DX:AX，对于32位操作为EDX:EAX，对于64位操作为RDX:RAX）。\n两个操作数 imul dest, source ​\t这种形式将source与dest相乘，并将结果存储在dest中。dest可以是一个寄存器或内存位置，而source可以是一个寄存器、内存位置或立即数。注意，结果被截断到目标操作数的大小。\n三个操作数 imul dest, source1, source2 ​\t这种形式将source1与source2相乘，并将结果存储在dest中。source1和source2可以是寄存器、内存位置或立即数，但dest必须是寄存器。结果同样被截断到目标操作数的大小。\nmul - 无符号整数乘法 mul的功能 ​\tMUL是一个单操作数指令，它隐式地使用AL（8位）、AX（16位）、EAX（32位）或RAX（64位）作为其中一个乘数，具体取决于操作数的大小和处理器模式。\n​\t对于字节乘法（8位），结果被存储在AX寄存器中；对于字乘法（16位），结果被存储在DX:AX寄存器对中；对于双字乘法（32位），结果被存储在EDX:EAX寄存器对中；对于四字乘法（64位），结果被存储在RDX:RAX寄存器对中。\nmul的示例 ; 8 mov al, 0x05 ; AL = 5 mov bl, 0x03 ; BL = 3 mul bl ; AX := AL * BL ; AL = 5 * 3 = 15 (0x0F), AH = 0 ; 16 mov ax, 0x0123 ; AX = 291 mov bx, 0x0004 ; BX = 4 mul bx ; DX:AX := AX * BX ; AX = 291 * 4 = 1164 (0x048C), DX = 0 ; 32 mov eax, 0x12345678 ; EAX = 305419896 mov ebx, 0x00000002 ; EBX = 2 mul ebx ; EDX:EAX := EAX * EBX ; EAX = 305419896 * 2 = 610839792 (0x2468ACF0), EDX = 0 scanf与printf函数 ​\tscanf会使用多个寄存器：rcx，rdx，r8等等，均存储输入项的地址，其中rcx为格式化字符串的地址。 ​\tprintf使用的寄存器与scanf类似。\n逆向分析程序1 ​\t使用IDA打开reverse_basic.exe，反汇编结果如下：\n; int __fastcall main(int argc, const char **argv, const char **envp) main proc near var_18= dword ptr -18h var_14= dword ptr -14h var_10= qword ptr -10h arg_0= qword ptr 8 arg_8= qword ptr 10h arg_10= qword ptr 18h ; __unwind { // __GSHandlerCheck mov [rsp+arg_0], rbx mov [rsp+arg_8], rbp mov [rsp+arg_10], rsi push rdi sub rsp, 30h mov rax, cs:__security_cookie xor rax, rsp mov [rsp+38h+var_10], rax lea r8, [rsp+38h+var_18] lea rdx, [rsp+38h+var_14] lea rcx, aDD ; \u0026#34;%d %d\u0026#34; call sub_140001080 mov ecx, [rsp+38h+var_18] mov eax, 38E38E39h add ecx, [rsp+38h+var_14] imul r8d, ecx, 7 lea rcx, Format ; \u0026#34;%d\\n\u0026#34; lea ebx, ds:0[r8*8] lea edi, [rbx+rbx*8] mul edi mov esi, edx mov edx, r8d shr esi, 1 mov ebp, esi shr ebp, 3 call sub_140001020 mov edx, ebx lea rcx, Format ; \u0026#34;%d\\n\u0026#34; call sub_140001020 mov edx, edi lea rcx, Format ; \u0026#34;%d\\n\u0026#34; call sub_140001020 mov edx, esi lea rcx, aU ; \u0026#34;%u\\n\u0026#34; call sub_140001020 mov edx, ebp lea rcx, aU ; \u0026#34;%u\\n\u0026#34; call sub_140001020 mov eax, 24924925h lea rcx, aU ; \u0026#34;%u\\n\u0026#34; mul ebp sub ebp, edx shr ebp, 1 add edx, ebp shr edx, 2 call sub_140001020 xor eax, eax mov rcx, [rsp+38h+var_10] xor rcx, rsp ; StackCookie call __security_check_cookie mov rbx, [rsp+38h+arg_0] mov rbp, [rsp+38h+arg_8] mov rsi, [rsp+38h+arg_10] add rsp, 30h pop rdi retn ; } // starts at 1400010E0 main endp ​\t可以发现该程序的结构大致是，进行一些运算之后，集中打印运算结果。其中sub_140001020即为prinf函数，sub_140001080为scanf函数。\nlea r8, [rsp+38h+var_18] lea rdx, [rsp+38h+var_14] lea rcx, aDD ; \u0026#34;%d %d\u0026#34; call sub_140001080 ​\t因此此程序会读取两个四字节数据写入var_14和var_18。\nmov ecx, [rsp+38h+var_18] mov eax, 38E38E39h add ecx, [rsp+38h+var_14] imul r8d, ecx, 7 ​\t分析接下来的指令，程序将读取的两个数相加之后乘以7，将结果存到r8d中。\nlea rcx, Format ; \u0026#34;%d\\n\u0026#34; lea ebx, ds:0[r8*8] lea edi, [rbx+rbx*8] mul edi mov esi, edx mov edx, r8d shr esi, 1 mov ebp, esi shr ebp, 3 call sub_140001020 ​\tlea ebx, ds:0[r8*8]通过地址计算的方式计算了r8 * 8的值存入ebx中，然后类似地计算rbx * 9存入edi中。\n​\t然后经过其他无关运算后，调用printf将r8d(乘以7的结果)打印出来（先将r8d赋给edx）。\nmov edx, ebx lea rcx, Format ; \u0026#34;%d\\n\u0026#34; call sub_140001020 mov edx, edi lea rcx, Format ; \u0026#34;%d\\n\u0026#34; call sub_140001020 ​\t这一段打印出ebx和edi。\nmov edx, esi lea rcx, aU ; \u0026#34;%u\\n\u0026#34; call sub_140001020 mov edx, ebp lea rcx, aU ; \u0026#34;%u\\n\u0026#34; call sub_140001020 ​\t根据这两个打印可以知道，在上面的运算中，esi和ebp被存入了运算结果，让我们再次分析上面被我们忽视的部分指令：\nmov eax, 38E38E39h ... lea rcx, Format ; \u0026#34;%d\\n\u0026#34; lea ebx, ds:0[r8*8] lea edi, [rbx+rbx*8] mul edi mov esi, edx mov edx, r8d shr esi, 1 mov ebp, esi shr ebp, 3 call sub_140001020 ​\tmul edi的作用：会计算edi * eax，将结果存入edx: eax中，eax被赋值为38E38E39h，是编译器将除以9运算进行优化使用的魔数。因此esi为edi / 9。然后将esi存入ebp后右移3位。\nmov eax, 24924925h lea rcx, aU ; \u0026#34;%u\\n\u0026#34; mul ebp sub ebp, edx shr ebp, 1 add edx, ebp shr edx, 2 call sub_140001020 ​\t24924925h是除以7的魔数，因此这里计算ebp除以7后打印出来。\n综上，我们可以复原这个可执行文件的原c文件：\n#include \u0026lt;stdio.h\u0026gt; int main(int argc, char *argv[]) { int a, b, c; scanf(\u0026#34;%d %d\u0026#34;, a, b); c = a + b; int n1 = c * 7; int n2 = n1 * 8; int n3 = n2 * 9; unsigned int n4 = n3 / 9; unsigned int n5 = n4 / 8; unsigned int n6 = n5 / 7; printf(\u0026#34;%d\\n\u0026#34;, n1); printf(\u0026#34;%d\\n\u0026#34;, n2); printf(\u0026#34;%d\\n\u0026#34;, n3); printf(\u0026#34;%u\\n\u0026#34;, n4); printf(\u0026#34;%u\\n\u0026#34;, n5); printf(\u0026#34;%u\\n\u0026#34;, n6); return 0; } 逆向分析程序2 ; int __fastcall main(int argc, const char **argv, const char **envp) main proc near var_18= dword ptr -18h var_14= dword ptr -14h var_10= qword ptr -10h arg_0= qword ptr 8 ; __unwind { // __GSHandlerCheck mov [rsp+arg_0], rbx push rdi sub rsp, 30h mov rax, cs:__security_cookie xor rax, rsp mov [rsp+38h+var_10], rax lea r8, [rsp+38h+var_14] lea rdx, [rsp+38h+var_18] lea rcx, aDD ; \u0026#34;%d %d\u0026#34; call sub_140001080 mov eax, [rsp+38h+var_14] lea rcx, Format ; \u0026#34;%d\\n\u0026#34; mov edi, [rsp+38h+var_18] lea ebx, [rdi+rax] imul edi, eax lea edx, [rdi+rbx] call sub_140001020 mov eax, 66666667h imul [rsp+38h+var_18] sar edx, 1 mov ecx, edx shr ecx, 1Fh add edx, ecx lea rcx, Format ; \u0026#34;%d\\n\u0026#34; call sub_140001020 mov r8d, [rsp+38h+var_14] lea rcx, Format ; \u0026#34;%d\\n\u0026#34; lea edx, [r8+r8*4] call sub_140001020 sub ebx, [rsp+38h+var_18] lea rcx, Format ; \u0026#34;%d\\n\u0026#34; mov edx, ebx call sub_140001020 mov eax, edi lea rcx, Format ; \u0026#34;%d\\n\u0026#34; cdq idiv [rsp+38h+var_14] mov edx, eax call sub_140001020 xor eax, eax mov rcx, [rsp+38h+var_10] xor rcx, rsp ; StackCookie call __security_check_cookie mov rbx, [rsp+38h+arg_0] add rsp, 30h pop rdi retn ; } // starts at 1400010E0 main endp lea r8, [rsp+38h+var_14] lea rdx, [rsp+38h+var_18] lea rcx, aDD ; \u0026#34;%d %d\u0026#34; call sub_140001080 ​\t容易知道，这里使用scanf函数读入两个数据存入var_18， var_14中。\nmov eax, [rsp+38h+var_14] lea rcx, Format ; \u0026#34;%d\\n\u0026#34; mov edi, [rsp+38h+var_18] lea ebx, [rdi+rax] imul edi, eax lea edx, [rdi+rbx] call sub_140001020 ​\t这段首先将var_14和var_18分别存入eax和edi中。计算ebx = rdi + rax，计算edi = edi * eax， 计算edx = rdi + rbx。即为计算edx = var_14 + var_18 + var_14 * var_18然后打印出来。\nmov eax, 66666667h imul [rsp+38h+var_18] sar edx, 1 mov ecx, edx shr ecx, 1Fh add edx, ecx lea rcx, Format ; \u0026#34;%d\\n\u0026#34; call sub_140001020 ​\t这里是使用魔数66666667h优化除法运算。实际上计算的是var_18 / 5。\nmov r8d, [rsp+38h+var_14] lea rcx, Format ; \u0026#34;%d\\n\u0026#34; lea edx, [r8+r8*4] call sub_140001020 ​\t容易知道这里计算的是var_14 * 5，使用var_14 * 4 + var_14优化。\nsub ebx, [rsp+38h+var_18] lea rcx, Format ; \u0026#34;%d\\n\u0026#34; mov edx, ebx call sub_140001020 mov eax, edi lea rcx, Format ; \u0026#34;%d\\n\u0026#34; cdq idiv [rsp+38h+var_14] mov edx, eax call sub_140001020 ​\tebx在之前存储着var_18 + var_14的结果，因此这里计算ebx - var_18后打印。\n​\tedi存储着var_18 * var_14的结果，这里计算var_18 * var14 / var_14后打印。\n源码：\n#include \u0026lt;stdio.h\u0026gt; int main() { int var_18, var_14; scanf(\u0026#34;%d %d\u0026#34;, \u0026amp;var_18, var_14); int a = var_18 + var_14; int b = var_18 * var_14; printf(\u0026#34;%d\\n\u0026#34;, a + b); printf(\u0026#34;%d\\n\u0026#34;, var_18 / 5); printf(\u0026#34;%d\\n\u0026#34;, var_14 * 5); printf(\u0026#34;%d\\n\u0026#34;, a - var_18); printf(\u0026#34;%d\\n\u0026#34;, b / var_14); return 0; } ","date":"2025-03-15T06:27:52Z","permalink":"https://xingfend.github.io/blog-by-hugo/post/reverse-program-course-week4/","title":"【武大软件逆向课程/第四周】加减乘除运算的逆向特征"},{"content":"作业目标 逆向分析扫雷程序winmine.exe，并编写程序对其进行批量插旗\n更换机器指令表.exe内的显示字体\n扫雷 定位绘制雷区的代码位置 使用Ollydbg打开winmin.exe。\n在当前模块中右键-\u0026gt;查找-\u0026gt;当前模块中的名称（标签），在弹出的窗口中寻找名称为Bitblt的函数。这里可以直接右键-\u0026gt;在每个参考上设置断点，但是我的Ollydbg会报错。因此采用后续的方法。\n选中Bitblt项后，按回车键Enter，在弹出的窗口中再次按回车Enter。\n执行上述操作后弹出的窗口即为Bitblt的源码，在源码首行按F2或者右键设置断点。\n设置断点后按F9或功能按钮运行程序，程序会暂停在断点处。此时按Ctrl + F9或者功能按钮执行到返回，程序会运行到函数Bitblt调用处的下一行。\n观察上下文程序可以发现是类似于一个循环的结构，其中esi寄存器为循环变量。\n定位雷区数据地址 在这一节内需要观察不同操作下内存区域的变化。\n在Ollydbg中可以通过单击任意数据来刷新数据窗口中的内容。\n如果想要重新绘制扫雷窗口，需要最小化后恢复窗口。\n观察内存读取部分，代码中只有两句可能是对于数组的读取。分别为：\nmov al, byte ptr [ebx+esi] push dword ptr [eax*4+1005A20] 这里可以记录一下Bitblt调用处的地址1002706，然后取消所有断点重新运行程序，将游戏切换到高级模式，随机点击一个地块。这里为了便于定位雷区数据位置，我们多次开局点击地块，直到仅连锁翻开一个地块（这样对雷区数据的影响较小，数据特征更明显）。\n然后将在数据窗口中右键-\u0026gt;转到-\u0026gt;表达式或者按Ctrl + G，输入内存读取部分的常量地址1005A20跳转到该地址。观察数据窗口内该地址处的数据。\n发现并不符合雷区数据的特征（至少应该出现大量相同的两种数据）。实际上作为绘制雷区的函数，对数组下标的访问应该具有循环变量，也就是esi寄存器应该参与内存地址的计算，而这条指令并不符合。\n因此选择下面的指令来寻找雷区数据所在的内存地址。\nmov al, byte ptr [ebx+esi] 在这条指令上打断点后最小化扫雷窗口，再恢复窗口，查看ebx寄存器的值，发现是内存地址1005360，在数据窗口中跟踪该地址。\n该地址处的内存数据如下。可以大致推测，未翻开的雷区和未翻开的空白对应数据为0x0F或0x8F。\n再次重开游戏，翻开第一个地块以定位数据区域首个元素的位置，如下图所示。\n对比可知1005361为首的地址存放雷区数据。\n分析地块状态对应的内存值 非雷区地块 从上一节的最后一个图可以得知，翻开的2地块对应的内存值为0x42。推测翻开的n对应的内存值为0x4n。\n翻开相邻地块进行验证，可知推测成立，同时也得知未翻开的非雷区对应的内存值为0x0F。\n雷区地块 再次观察内存区域，在9个地块后，有一个地块的内存值为不同的0x8F，翻开该地块后再次观察内存区域变化。\n可知未翻开的雷为0x8F，翻开的雷为0x8A，踩中的雷为0xCC。\n插旗地块与存疑地块 重开游戏并刷新数据窗口。在任意0x8E和0x0F对应的地块上右键以插旗后，刷新数据窗口，观察对应数据变化。扫雷游戏中还有一种标记，在插旗的地块上再次右键可以变为问号地块，这里称为存疑地块，与本次任务并不相关，但是这里还是给出其对应的内存值。\n观察上图可知，插旗地块的0x?F被替换为0x?E，存疑地块的0x?F被替换为0x?D。\n至此，所有地块状态对应的内存值均被我们分析出来。\n简单总结一下，地块状态对应的内存值如下：\n地块状态 内存值 地块状态 内存值 未翻开的空地 0x0F 插旗的空地 0x0E 翻开的空地 0x4n(n为要显示的数字) 存疑的空地 0x0D 未翻开的地雷 0x8F 插旗的地雷 0x8E 翻开的地雷 0x8A 存疑的地雷 0x8D 踩中的地雷 0xCC 此外，按Ctrl + ↓调整数据窗口首列数据从0x01偏移开始展示，可以发现在高级模式下30 * 16的布局下，每30个地块的数据后都有两个0x10，用于标注此行结束，在后面编写程序时，应当注意处理这个间隔区域。\n编写程序以批量插旗地雷 程序如下：\n#include \u0026lt;windows.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;tchar.h\u0026gt; #include \u0026lt;tlhelp32.h\u0026gt; #define GET_OFFSET(ROW, COL) ((ROW) * 32 + (COL)) void print_sign(BYTE val) { if (val == 0x0F || val == 0x8F) { printf(\u0026#34;--\u0026#34;); } else if (val == 0x0E || val == 0x8E) { printf(\u0026#34;||\u0026#34;); } else if (val == 0x0D || val == 0x8D) { printf(\u0026#34;??\u0026#34;); } else if (val == 0x40) { printf(\u0026#34;..\u0026#34;); } else if (val == 0x8A) { printf(\u0026#34;**\u0026#34;); } else if (val == 0xCC) { printf(\u0026#34;XX\u0026#34;); } else if (val == 0x10) { } else { printf(\u0026#34;%01X\u0026#34;, val \u0026amp; 0x0F); printf(\u0026#34;%01X\u0026#34;, val \u0026amp; 0x0F); } printf(\u0026#34; \u0026#34;); } int main() { DWORD pid = 0; // Target process ID HANDLE hProcess = NULL; LPVOID targetAddress = (LPVOID)0x1005361; // Target memory address BYTE readBuffer[512] = {0}; // BYTE writeData[4] = {0xAA, 0xBB, 0xCC, 0xDD}; // Example data to write SIZE_T bytesRead = 0; // SIZE_T bytesWritten = 0; // Create a snapshot of all processes HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (hSnapshot == INVALID_HANDLE_VALUE) { printf(\u0026#34;Failed to create process snapshot, error code: %d\\n\u0026#34;, GetLastError()); return 1; } PROCESSENTRY32 pe = {sizeof(PROCESSENTRY32)}; if (Process32First(hSnapshot, \u0026amp;pe)) { do { if (_tcsicmp(pe.szExeFile, _T(\u0026#34;winmine.exe\u0026#34;)) == 0) { pid = pe.th32ProcessID; printf(\u0026#34;Process found: winmine.exe, PID: %u\\n\u0026#34;, pid); break; } } while (Process32Next(hSnapshot, \u0026amp;pe)); } else { printf(\u0026#34;Failed to retrieve process information, error code: %d\\n\u0026#34;, GetLastError()); } CloseHandle(hSnapshot); if (pid == 0) { printf(\u0026#34;Process winmine.exe not found\\n\u0026#34;); return 1; } // Open the target process with required permissions for memory operations hProcess = OpenProcess( PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid ); if (hProcess == NULL) { printf(\u0026#34;Failed to open process, error code: %d\\n\u0026#34;, GetLastError()); return 1; } // Read memory content if (!ReadProcessMemory( hProcess, targetAddress, readBuffer, sizeof(readBuffer), \u0026amp;bytesRead )) { printf(\u0026#34;Failed to read memory, error code: %d\\n\u0026#34;, GetLastError()); CloseHandle(hProcess); return 1; } printf(\u0026#34;Read %d bytes of data:\\n\u0026#34;, bytesRead); for (int i = 0; i \u0026lt; 16; i++) { for (int j = 0; j \u0026lt; 32; j++) { print_sign(readBuffer[GET_OFFSET(i, j)]); } printf(\u0026#34;\\n\u0026#34;); } printf(\u0026#34;Start flaging all the mines..\\n\u0026#34;); // Write to memory int count = 0; for (int i = 0; i \u0026lt; 16; i++) { for (int j = 0; j \u0026lt; 32; j++) { BYTE flag_mine = 0x8E; if (readBuffer[GET_OFFSET(i, j)] == 0x8F) { WriteProcessMemory( hProcess, targetAddress + GET_OFFSET(i, j), \u0026amp;flag_mine, sizeof(flag_mine), NULL ); count++; } } } printf(\u0026#34;Finish! the count of mines: %d\\n\u0026#34;, count); CloseHandle(hProcess); // system(\u0026#34;pause\u0026#34;); return 0; } 这段程序将会在打印雷区当前状态后，将所有0x8F修改为0x8E，即将所有的未翻开的地雷插上旗子，用户应当在新一局游戏内翻开任意地块后运行此程序，然后最小化再恢复窗口以重新绘制雷区。\n打印当前状态过程中，未翻开的所有地块将记为--，插旗地块记为||，翻开的空地记为..，翻开的数字地块记为该数字，翻开的地雷记为**，踩中的地雷记为XX。\n实验结果 运行结果如下图所示：\n刷新窗口后成功插旗。\n降低雷数为20后重新运行，便于验证结果正确性，手动翻开所有未插旗地块，中间笑脸图标带上墨镜（表示游戏胜利），可见结果正确。\n更换机器指令表.exe显示字体 定位控制字体显示的WinAPI 使用Ollydbg打开机器指令表.exe。\n在API文档及网络资料中查询其内涉及的API。\nAPI名称 功能 GetModuleHandle 检索指定模块的模块句柄。模块必须已由调用进程加载。 GetCommandLine 检索当前进程的命令行字符串。 ExitProcess 结束调用进程及其所有线程。 GetSystemMetrics 用于获取关于显示器、鼠标、键盘等系统参数的信息。 观察发现在程序有效的程序段中涉及的三个WinAPI均与字体显示无关。还剩下一个call 004031，推测其很可能控制字体的显示。其上的四个push可能包含参数，尝试修改为push 0D 观察程序变化。修改两个push后运行程序并无明显变化。可见这两个参数并不控制该程序的字体显示。\n在call 00401031处设断点并单步步入，依次检索遇见的到的WinAPI功能。\nAPI名称 功能 CreateWindowEx 用于创建一个窗口（包括各种控件如按钮、文本框等）。 SetWindowLong 更改指定窗口的属性。该函数还将指定偏移量处的32位(长)值设置为额外的窗口内存。 GetStockObject 用于获取预定义的图形对象（如画笔、画刷和字体）的句柄。 其中GetStockObject涉及字体的显示，在4010C6处调用，其上一条指令push 0B很可能控制字体的显示。\n将push 0B修改为push 0E后继续运行程序，可见字体明显发生变化。\n将修改保存到文件 选中被修改的指令，在指令窗口中右键-\u0026gt;复制到可执行文件-\u0026gt;选择，在弹出的新窗口中选中被修改的指令，右键-\u0026gt;保存文件。将新可执行文件命名为机器指令表2.exe。\n双击运行修改前后的机器指令表，对比两者的字体差异。\n","date":"2025-02-19T17:43:52+08:00","permalink":"https://xingfend.github.io/blog-by-hugo/post/reverse-program-course-week1/","title":"【武大软件逆向课程/第一周】扫雷逆向分析批量插旗及机器指令表.exe文件字体更换"},{"content":"计划 添加widget，展示当前博文的tags 右侧边栏的tag-cloud和categories不能处理hidden博文 tag-cloud识别hidden博文 把layouts/partials/widget/tag-cloud.html的渲染tag-cloud的逻辑进行如下修改：\n\u0026lt;div class=\u0026#34;tagCloud-tags\u0026#34;\u0026gt; - {{ range first $limit $context.Site.Taxonomies.tags.ByCount }} - \u0026lt;a href=\u0026#34;{{ .Page.RelPermalink }}\u0026#34; class=\u0026#34;font_size_{{ .Count }}\u0026#34;\u0026gt; - {{ .Page.Title }} - \u0026lt;/a\u0026gt; + {{- $cateTaxonomy := $context.Site.GetPage \u0026#34;tags\u0026#34; -}} + {{- $cateTerms := $cateTaxonomy.Pages -}} + {{ range first $limit $cateTerms }} + {{- $pages := .Pages -}} + {{- $filteredPages := where $pages \u0026#34;.Params.hidden\u0026#34; \u0026#34;!=\u0026#34; true -}} + {{ if $filteredPages }} + \u0026lt;a href=\u0026#34;{{ .RelPermalink }}\u0026#34; class=\u0026#34;font_size_{{ len $filteredPages}}\u0026#34;\u0026gt; + {{ .Title }} + \u0026lt;/a\u0026gt; + {{ end }} + {{ end }} \u0026lt;/div\u0026gt; 如另一篇博文中的方法，从$context.Site中获取所有tags然后过滤掉hidden == true的博文后，判断$filteredPages中是否还有元素，如果有则渲染这个tag。\n添加一种展示当前博文tags的Widget 添加模板文件 在layouts/partials/widget文件夹下新建文件cur-tags.html，添加如下内容：\n{{- $context := .Context -}} {{ if .Context.Page.Params.Tags }} \u0026lt;section class=\u0026#34;widget curTags\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;widget-icon\u0026#34;\u0026gt; {{ partial \u0026#34;helper/icon\u0026#34; \u0026#34;tag\u0026#34; }} \u0026lt;/div\u0026gt; \u0026lt;h2 class=\u0026#34;widget-title section-title\u0026#34;\u0026gt;{{ T \u0026#34;widget.curTags.title\u0026#34; }}\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;curTags-tags\u0026#34;\u0026gt; {{ range (.Context.Page.GetTerms \u0026#34;tags\u0026#34;) }} \u0026lt;a href=\u0026#34;{{ .RelPermalink }}\u0026#34;\u0026gt; {{ .LinkTitle }} \u0026lt;/a\u0026gt; {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/section\u0026gt; {{ end }} 首先判断当前文章是否有tag，然后根据其他widget的模板修改成current tags，使用GetTerms方法获得当前文章的tags，作为\u0026lt;a\u0026gt;标签的参数。\n注意\u0026lt;h2\u0026gt;标签中的T函数，用于在修改页面语言时批量替换文本，我们需要在对应文件中添加widget.curTags.title命名空间的值。\n添加i18n替换文本 在i18n/en.yaml和i18n/zh-cn.yaml中添加相关内容，在widget域下添加内容：\nwidget: + curTags: + title: + other: Tags of current blog 添加scss样式表 在assets/scss/partials/widgets.scss中添加curTags的样式。\n/* Current tags widget */ .curTags { .curTags-tags { display: flex; flex-wrap: wrap; gap: 10px; a { background: var(--card-background); box-shadow: var(--shadow-l1); border-radius: var(--tag-border-radius); padding: 8px 20px; color: var(--card-text-color-main); font-size: 1.4rem; transition: box-shadow 0.3s ease; \u0026amp;:hover { box-shadow: var(--shadow-l2); } } } } 自此，current tags控件的开发已经成功，在stack/config.yaml中添加配置项即可。\nparams: widgets: page: + - type: cur-tags ","date":"2025-02-12T16:33:24+08:00","image":"https://xingfend.github.io/blog-by-hugo/post/blogbuild2-widgets/blogbuild2_hu_673e5935641026e3.png","permalink":"https://xingfend.github.io/blog-by-hugo/post/blogbuild2-widgets/","title":"Widgets修复与开发"},{"content":"Archive页介绍 archive页面展示了当前博文的所有类型，在此之下依据年份时序展示所有博文。\n改造计划:\n添加对以tag分类的支持\n支持在archive页展示tag模块\n修改tag\u0026ndash;tile的大小\n修改tag\u0026ndash;tile的排列方式\n为tag分类的每一个tile添加不同颜色\n修复在archive页面的category tile展示异常\n修复category详细页的文章数量异常\n添加archive页面对应配置项\n使用开发者工具检查archive页面元素 发现category分类的元素为：\n\u0026lt;h2 class=\u0026#34;section-title\u0026#34;\u0026gt;类别 - Categories\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;subsection-list\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;article-list--tile\u0026#34;\u0026gt; \u0026lt;article\u0026gt; \u0026lt;/article\u0026gt; \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; 显然\u0026lt;div class=\u0026quot;article-list--tile\u0026quot;\u0026gt;的class属性命名并不合理，因为在archive首页展示的category列表的每一个tile并不是一篇article，因此应当修改为\u0026lt;div class=\u0026quot;category-list--tile\u0026quot;\u0026gt;。\n在文件themes/hugo-theme-stack/layouts/_default/archives.html中将\u0026lt;div class=\u0026quot;article-list--tile\u0026quot;\u0026gt;修改为\u0026lt;div class=\u0026quot;category-list--tile\u0026quot;\u0026gt;。\n同时将下列文件中的选择器article-list--tile修改为category-list--tile。在scss文件中建议保留原有的选择器，新增修改名称后的选择器。\nthemes/hugo-theme-stack/assets/scss/partials/article.scss themes/hugo-theme-stack/assets/scss/partials/layout/list.scss /home/fendy/Project/BlogSrc/themes/hugo-theme-stack/assets/ts/main.ts 修复category tile展示异常 本质上是没有添加对hidden文章的判断逻辑，在themes/hugo-theme-stack/layouts/_default/archives.html中添加筛选逻辑：\n\u0026lt;h2 class=\u0026#34;section-title\u0026#34;\u0026gt;类别 - {{ $cateTaxonomy.Title }}\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;subsection-list\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;category-list--tile\u0026#34;\u0026gt; {{ range $cateTerms }} {{- $pages := .Pages -}} {{- $filteredPages := where $pages \u0026#34;.Params.hidden\u0026#34; \u0026#34;!=\u0026#34; true -}} {{ if $filteredPages }} {{ partial \u0026#34;article-list/tile\u0026#34; (dict \u0026#34;context\u0026#34; . \u0026#34;size\u0026#34; \u0026#34;250x150\u0026#34; \u0026#34;Type\u0026#34; \u0026#34;taxonomy\u0026#34;) }} {{ end }} {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; 添加对tag分类的支持 支持在archive页展示tag模块 在themes/hugo-theme-stack/layouts/_default/archives.html中，类别模块的下方添加如下代码：\n{{- $tagTaxonomy := $.Site.GetPage \u0026#34;taxonomyTerm\u0026#34; \u0026#34;tags\u0026#34; -}} {{- $tagTerms := $tagTaxonomy.Pages -}} {{ if $tagTerms }} \u0026lt;h2 class=\u0026#34;section-title\u0026#34;\u0026gt;标签 - {{ $tagTaxonomy.Title }}\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;subsection-list\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;tag-list--tile\u0026#34;\u0026gt; {{ range $tagTerms }} {{/* 获取当前标签下的所有页面 */}} {{- $pages := .Pages -}} {{/* 过滤掉 hidden 为 true 的页面 */}} {{- $filteredPages := where $pages \u0026#34;.Params.hidden\u0026#34; \u0026#34;!=\u0026#34; true -}} {{ if $filteredPages }} {{ partial \u0026#34;article-list/tile\u0026#34; (dict \u0026#34;context\u0026#34; . \u0026#34;Type\u0026#34; \u0026#34;tag\u0026#34; \u0026#34;pages\u0026#34; $filteredPages) }} {{ end }} {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; {{ end }} 添加tag的样式并修改tag\u0026ndash;tile的大小 在themes/hugo-theme-stack/assets/scss/partials/article.scss中添加:\n.tag-list--tile { article { border-radius: var(--card-border-radius); overflow: hidden; position: relative; height: 350px; width: 250px; box-shadow: var(--shadow-l1); transition: box-shadow 0.3s ease; background-color: var(--card-background); \u0026amp;:hover { box-shadow: var(--shadow-l2); } .article-details { border-radius: var(--card-border-radius); position: relative; height: 100%; width: 100%; display: flex; flex-direction: column; justify-content: flex-end; z-index: 2; padding: 15px; @include respond(sm) { padding: 20px; } } .article-title { font-size: 2rem; font-weight: 500; color: var(--card-text-color-main); @include respond(sm) { font-size: 2.2rem; } } } } 在themes/hugo-theme-stack/assets/scss/partials/layout/list.scss中添加:\n.tag-list--tile { display: flex; article { width: 120px; height: 30px; margin-right: 20px; flex-shrink: 0; .article-title { margin: 0; font-size: 1.8rem; } .article-details { padding-bottom: 5px; text-align: center; } } } 添加archives页面对应配置项 设计配置项 由于目前archives页面有三个板块：Categories，Tags，Dates。考虑添加如下配置项（标注出的为默认配置，不进行配置也会以下面代码块给定的配置渲染）：\nparams: archives: showCategories: true showTags: false showDates: true 修改对应源码读取配置项 修改layouts/_default/archives.html文件，将{{ if $term }}判断语句添加一个与项，同时修改变量名。最终修改后结果如下：\n{{ if and (eq (.Site.Params.archives.showCategories | default true) true) (gt (len $cateTerms) 0) }} 同理，在渲染tags和dates板块的部分使用如下语句对包裹：\n{{ if and (eq (.Site.Params.archives.showTags | default false) true) (gt (len $tagTerms) 0) }} {{ /* 渲染Tags板块 */ }} {{ end }} {{ if eq (.Site.Params.archives.showDates | default true) true }} {{ /* 渲染Dates板块*/ }} {{ end }} 修复category详细页的文章数量异常问题 在layouts/_default/list.html文件中进行如下修改：\n- \u0026lt;h3 class=\u0026#34;section-count\u0026#34;\u0026gt;{{ T \u0026#34;list.page\u0026#34; (len .Pages) }}\u0026lt;/h3\u0026gt; + {{- $filteredPages := where .Pages \u0026#34;.Params.hidden\u0026#34; \u0026#34;!=\u0026#34; true -}} + \u0026lt;h3 class=\u0026#34;section-count\u0026#34;\u0026gt;{{ T \u0026#34;list.page\u0026#34; (len $filteredPages) }}\u0026lt;/h3\u0026gt; 把.Pages的元素个数替换为筛选后的元素个数。\n新增分类方式：collections 设计配置项 在layouts/_default/archives.html中添加配置项showCollections。\nparams: archives: showCategories: true + showCollections: true showTags: false showDates: true 修改archive.html渲染collections列表 \u0026lt;!-- Collections Section --\u0026gt; {{- $collectionTaxonomy := $.Site.GetPage \u0026#34;collections\u0026#34; -}} {{- $collectionTerms := $collectionTaxonomy.Pages -}} {{ if and (eq (.Site.Params.archives.showCollections | default false) true) (gt (len $collectionTerms) 0) }} \u0026lt;h2 class=\u0026#34;section-title\u0026#34;\u0026gt;合集 - {{ $collectionTaxonomy.Title }}\u0026lt;/h2\u0026gt; \u0026lt;div class=\u0026#34;subsection-list\u0026#34;\u0026gt; \u0026lt;div class=\u0026#34;category-list--tile\u0026#34;\u0026gt; {{ range $collectionTerms }} {{- $pages := .Pages -}} {{- $filteredPages := where $pages \u0026#34;.Params.hidden\u0026#34; \u0026#34;!=\u0026#34; true -}} {{ if $filteredPages }} {{ partial \u0026#34;article-list/tile\u0026#34; (dict \u0026#34;context\u0026#34; . \u0026#34;size\u0026#34; \u0026#34;250x150\u0026#34; \u0026#34;Type\u0026#34; \u0026#34;taxonomy\u0026#34;) }} {{ end }} {{ end }} \u0026lt;/div\u0026gt; \u0026lt;/div\u0026gt; {{ end }} 在文件layouts/_default/archives.html中需要的位置添加如上代码。这篇博客是在categories和tags中间添加。\n修改配置文件 修改hugo.toml添加如下配置项。\n[taxonomies] category = \u0026#39;categories\u0026#39; tag = \u0026#39;tags\u0026#39; collection = \u0026#39;collections\u0026#39; 新建模板文件 在content文件夹下新建文件夹collections，在collections文件夹下如category一般新建分类项。如新建子目录及文件test-collection/_index.md，并在子目录中添加如下内容：\n--- title: \u0026#34;测试用\u0026#34; description: \u0026#34;test\u0026#34; --- 在这之后就可以在博文的front-matter中添加如下内容，以将这篇博文归类到一个合集中：\ncollections = \u0026#34;test-collection\u0026#34; 效果展示 新建合集rvs-enr-crs选择一篇博文添加collections = \u0026quot;rvs-env-crs\u0026quot;。\n可见成功生效。\n","date":"2025-02-02T22:46:55+08:00","image":"https://xingfend.github.io/blog-by-hugo/post/blogbuild1-archive/blogbuild1_hu_57001f4b0e4944d8.png","permalink":"https://xingfend.github.io/blog-by-hugo/post/blogbuild1-archive/","title":"Archive页改造"},{"content":" 此博客搭建过程参考的资料在\u0026quot;关于\u0026quot;页面可详细了解。本文具体步骤也可能会给出具体参考的内容。\n不能正确渲染主题 - 2025.01.28 此博客采用源码-博客页分离的结构搭建。\n源码为hugo个人博客的源码，设为github私有仓库，通过git action，在每次push之后，通过hugo -D命令云端编译生成public文件夹中的内容之后将public文件夹内的内容推送到GitHub - XingfenD/xingfen-star.github.io仓库中。\n在搭建阶段，我将themes子文件夹下的第三方主题仓库设置为该主项目的子模块，导致在上传到私有仓库后，themes文件夹中没有实际的主题文件。最终导致无法编译出所选主题的页面。\n我在github action中添加步骤Print Directory以打印路径信息，最终定位此问题并修复，完整的githu action如下。\n最终博客项目的结构为三个仓库：\nHugo源码仓库(通过gitignore忽略public, resources, themes文件夹)\n主题仓库(Hugo源码仓库的public文件夹)\nGithub Page仓库(Hugo源码仓库的public文件夹)\nname: deploy # 代码提交到main分支时触发github action on: push: branches: - main jobs: deploy: runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Hugo uses: peaceiris/actions-hugo@v3 with: hugo-version: \u0026#34;latest\u0026#34; extended: true - name: Print Directory run: tree -L 3 -d - name: Clone Theme run: git clone https://github.com/XingfenD/hugo-theme-custom.git themes - name: Git Configuration run: | git config --global core.quotePath false git config --global core.autocrlf false git config --global core.safecrlf true git config --global core.ignorecase false - name: Build Web run: hugo -D - name: Print Directory run: tree -L 3 -d - name: Deploy Web uses: peaceiris/actions-gh-pages@v4 with: personal_token: ${{ secrets.ACTION_ACCESS_TOKEN }} external_repository: XingfenD/xingfen-star.github.io publish_branch: main publish_dir: public commit_message: auto deploy 主题配置后代码块显示异常 - 2025.02.02 使用hugo-theme-stack主题后\n发现在白天模式下的代码块颜色异常，如下图所示：\n在通过修改hugo-theme-stack主题的scss文件后成功定位异常原因：\nhugo编译md文件后生成的博文html文件中的codeblock为如下结构: \u0026lt;div class=\u0026#34;highlight\u0026#34;\u0026gt; \u0026lt;pre style=\u0026#34;\u0026#34;\u0026gt; \u0026lt;code class=\u0026#34;language-markdown\u0026#34;\u0026gt; \u0026lt;span class=\u0026#34;line\u0026#34;\u0026gt; ... \u0026lt;/code\u0026gt; \u0026lt;/pre\u0026gt; \u0026lt;/div\u0026gt; 图中所示的深色异常颜色为\u0026lt;pre\u0026gt;标签的style属性控制，因此导致全局scss文件失效。\n阅读官方文档后，在`hugo.toml`中添加配置： markup.highlight.noClasses=false 问题得以修复。\n修改配置文件以激活右侧边栏 - 2025.02.12 阅读Stack主题官方文档右侧边栏配置项可以了解如何修改配置文件以激活右边栏。\n在config.yaml文件中修改配置项[params.widgets]:\nparams: widgets: homepage: - type: search - type: categories params: limit: 10 - type: tag-cloud params: limit: 10 page: - type: toc - type: categories params: limit: 5 - type: tag-cloud params: limit: 5 配置生效后右侧边栏效果如下图:\n不能正确渲染abbr, sub等特殊html标签 - 2025.02.12 查阅在线资料修复Hugo静态生成器中忽略原始HTML的方法后，在hugo.toml中添加配置项：\nmarkup.goldmark.renderer.unsafe=true 成功渲染，效果如图。\n","date":"2025-02-02T17:21:58+08:00","image":"https://xingfend.github.io/blog-by-hugo/post/hugo-githubio/facebook_cover_photo_2_hu_6ce2a718f268c9db.png","permalink":"https://xingfend.github.io/blog-by-hugo/post/hugo-githubio/","title":"hugo+GithubIO自建博客踩坑实录"},{"content":"本文提供了可以在 Hugo 内容文件中使用的最基本 Markdown 语法样本，同时也展示了基本 HTML 元素是否可以通过 Hugo 主题中的 CSS 被装饰。\n标题 以下 HTML \u0026lt;h1\u0026gt;—\u0026lt;h6\u0026gt; 元素代表六个级别的章节标题。\u0026lt;h1\u0026gt; 是最高级别，而 \u0026lt;h6\u0026gt; 是最低级别。\n# H1 ## H2 ### H3 #### H4 ##### H5 ###### H6 H1 H2 H3 H4 H5 H6 段落 Xerum, quo qui aut unt expliquam qui dolut labo. Aque venitatiusda cum, voluptionse latur sitiae dolessi aut parist aut dollo enim qui voluptate ma dolestendit peritin re plis aut quas inctum laceat est volestemque commosa as cus endigna tectur, offic to cor sequas etum rerum idem sintibus eiur? Quianimin porecus evelectur, cum que nis nust voloribus ratem aut omnimi, sitatur? Quiatem. Nam, omnis sum am facea corem alique molestrunt et eos evelece arcillit ut aut eos eos nus, sin conecerem erum fuga. Ri oditatquam, ad quibus unda veliamenimin cusam et facea ipsamus es exerum sitate dolores editium rerore eost, temped molorro ratiae volorro te reribus dolorer sperchicium faceata tiustia prat.\nItatur? Quiatae cullecum rem ent aut odis in re eossequodi nonsequ idebis ne sapicia is sinveli squiatum, core et que aut hariosam ex eat.\n引用块 引用块元素表示来自另一个源的内容，并且可选地带有必须在 footer 或 cite 元素内的引用，以及可选的如注释和缩写的行内更改。\n无属性的引用块 Tiam, ad mint andaepu dandae nostion secatur sequo quae. 注意 在引用块中可以使用 Markdown 语法。\n带有属性的引用块 \u0026gt; 不要通过共享内存来通信，而是通过通信来共享内存。\u0026lt;br\u0026gt; \u0026gt; — \u0026lt;cite\u0026gt;Rob Pike[^1]\u0026lt;/cite\u0026gt; [^1]: 上述引用摘自 Rob Pike 在 Gopherfest 2015 年 11 月 18 日[演讲](https://www.youtube.com/watch?v=PAAkCSZUG1c)的部分内容。 不要通过共享内存来通信，而是通过通信来共享内存。\n— Rob Pike1\n表格 表格不属于核心 Markdown 规范的一部分，但 Hugo 开箱即用地支持它们。\n名称 年龄 Bob 27 Alice 23 表格中的内联 Markdown 斜体 粗体 代码 斜体 粗体 代码 A B C D E F Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus ultricies, sapien non euismod aliquam, dui ligula tincidunt odio, at accumsan nulla sapien eget ex. Proin eleifend dictum ipsum, non euismod ipsum pulvinar et. Vivamus sollicitudin, quam in pulvinar aliquam, metus elit pretium purus Proin sit amet velit nec enim imperdiet vehicula. Ut bibendum vestibulum quam, eu egestas turpis gravida nec Sed scelerisque nec turpis vel viverra. Vivamus vitae pretium sapien 代码块 使用反引号的代码块 \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;示例 HTML5 文档\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;测试\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 使用四个空格缩进的代码块 \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;示例 HTML5 文档\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;测试\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026quot;en\u0026quot;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026quot;utf-8\u0026quot;\u0026gt; \u0026lt;title\u0026gt;示例 HTML5 文档\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;测试\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; 使用 Hugo 的内部 highlight 短代码的代码块 \u0026lt;!doctype html\u0026gt; \u0026lt;html lang=\u0026#34;en\u0026#34;\u0026gt; \u0026lt;head\u0026gt; \u0026lt;meta charset=\u0026#34;utf-8\u0026#34;\u0026gt; \u0026lt;title\u0026gt;示例 HTML5 文档\u0026lt;/title\u0026gt; \u0026lt;/head\u0026gt; \u0026lt;body\u0026gt; \u0026lt;p\u0026gt;测试\u0026lt;/p\u0026gt; \u0026lt;/body\u0026gt; \u0026lt;/html\u0026gt; Diff 代码块 [dependencies.bevy] git = \u0026#34;https://github.com/bevyengine/bevy\u0026#34; rev = \u0026#34;11f52b8c72fc3a568e8bb4a4cd1f3eb025ac2e13\u0026#34; - features = [\u0026#34;dynamic\u0026#34;] + features = [\u0026#34;jpeg\u0026#34;, \u0026#34;dynamic\u0026#34;] 列表类型 有序列表 第一项 第二项 第三项 无序列表 列表项 另一个项目 再一个项目 嵌套列表 水果 苹果 橙子 香蕉 奶制品 牛奶 奶酪 其他元素——abbr, sub, sup, kbd, mark \u0026lt;abbr title=\u0026#34;图形交换格式\u0026#34;\u0026gt;GIF\u0026lt;/abbr\u0026gt; 是一种位图图像格式。 H\u0026lt;sub\u0026gt;2\u0026lt;/sub\u0026gt;O X\u0026lt;sup\u0026gt;n\u0026lt;/sup\u0026gt; + Y\u0026lt;sup\u0026gt;n\u0026lt;/sup\u0026gt; = Z\u0026lt;sup\u0026gt;n\u0026lt;/sup\u0026gt; 按下 \u0026lt;kbd\u0026gt;CTRL\u0026lt;/kbd\u0026gt; + \u0026lt;kbd\u0026gt;ALT\u0026lt;/kbd\u0026gt; + \u0026lt;kbd\u0026gt;Delete\u0026lt;/kbd\u0026gt; 结束会话。 大多数 \u0026lt;mark\u0026gt;蝾螈\u0026lt;/mark\u0026gt; 是夜行性的，并猎食昆虫、蠕虫和其他小生物。 GIF 是一种位图图像格式。\nH2O\nXn + Yn = Zn\n按下 CTRL + ALT + Delete 结束会话。\n大多数 蝾螈 是夜行性的，并猎食昆虫、蠕虫和其他小生物。\n超链接图片 上述引用摘自 Rob Pike 在 Gopherfest 2015 年 11 月 18 日演讲的部分内容。\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"2025-02-02T00:00:00Z","image":"https://xingfend.github.io/blog-by-hugo/post/markdown-syntax/pawel-czerwinski-8uZPynIu-rQ-unsplash_hu_9e6bcf9cfe9a9448.jpg","permalink":"https://xingfend.github.io/blog-by-hugo/post/markdown-syntax/","title":"Markdown 语法指南"},{"content":"getopt.h简介 getopt.h 是C语言中一个用于处理命令行参数的内嵌库。可以通过以下两个函数来调用其功能：\ngetopt()\ngetopt_long()\n函数 getopt() getopt() 用于解析仅包含短选项的指令，例如 -g、-o、-f。\ngetopt() 示例程序 使用 getopt() 的参数处理框架如下：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;getopt.h\u0026gt; int main(int argc, char *argv[]) { int opt; /* parse the command line input */ while ((opt = getopt(argc, argv, \u0026#34;f:o:kh\u0026#34;)) != -1) { switch (opt) { case \u0026#39;f\u0026#39;: printf(\u0026#34;Arg -f: %s\\n\u0026#34;, optarg); break; case \u0026#39;o\u0026#39;: printf(\u0026#34;Arg -o: %s\\n\u0026#34;, optarg); break; case \u0026#39;k\u0026#39;: printf(\u0026#34;Arg -k do not need arguments:%s\\n\u0026#34;, optarg); break; case \u0026#39;h\u0026#39;: printf(\u0026#34;Arg -k do not need arguments:%s\\n\u0026#34;, optarg); break; case \u0026#39;?\u0026#39;: // An invalid option was found or a required argument is missing. if (optopt == \u0026#39;f\u0026#39; || optopt == \u0026#39;o\u0026#39;) { printf(\u0026#34;Option -%c requires an argument.\\n\u0026#34;, optopt); } else { printf(\u0026#34;Unknown option -%c.\\n\u0026#34;, optopt); } break; default: printf(\u0026#34;Unexpected situation.\\n\u0026#34;); break; } } /* extra arguments handling */ if (optind \u0026lt; argc) { printf(\u0026#34;Remaining arguments: \u0026#34;); for (; optind \u0026lt; argc; optind++) { printf(\u0026#34;%s \u0026#34;, argv[optind]); } } /* optind \u0026lt; argc */ } 编译并运行如下指令：./test_short.out -v yuanshen -f arg:f -o arg:o -k arg:k -h arg:h\n\u0026gt; ./test_short.out -v yuanshen -f arg:f -o arg:o -k arg:k -h arg:h ./test_short.out: invalid option -- \u0026#39;v\u0026#39; Invalid option: v Arg -f: arg:f Arg -o: arg:o Arg -k do not need arguments:(null) Arg -k do not need arguments:(null) Remaining arguments: yuanshen arg:k arg:h % getopt()详细说明 全局变量 全局变量 用途 optopt (int) 遇到未知选项或缺少必要参数时设置，存储导致错误的选项字符。 optarg (char*) 存储当前选项的参数值。当解析需要参数的选项（例如 -f file.txt 中的 file.txt）时，optarg 指向该字符串。 optind (int) 在解析命令行参数过程中，追踪位置，具体指示下一个要处理的参数的索引。 以下代码段会打印所有未被 getopt() 识别的参数（忽略这些参数的输入顺序）：\n/* extra arguments handling */ if (optind \u0026lt; argc) { printf(\u0026#34;Remaining arguments: \u0026#34;); for (; optind \u0026lt; argc; optind++) { printf(\u0026#34;%s \u0026#34;, argv[optind]); } } 模式字符串 模式字符串的定义是 const char *__shortopts，在示例程序中，其实例为 \u0026quot;f:o:kh\u0026quot;。\n顾名思义，模式字符串定义了 getopt() 和 getopt_long() 函数中将被识别的短选项。冒号 : 跟在短选项字符后表示该选项需要一个参数。\n以 \u0026quot;f:o:kh\u0026quot; 为例，此模式字符串接收4个选项。选项 -f 和 -o 需要输入参数，而选项 -k 和 -h 不需要。\n注意：如果 -f 后跟一个选项，该选项会被视为参数。\n函数 getopt_long() getopt_long() 用于解析既包含短选项（如 -g、-o、-f）又包含长选项（如 --version、--input）的指令。\n函数 getopt_long 识别一个结构体数组 struct option，该数组定义了长选项的信息。\nstruct option 的原型及相关介绍如下：\nstruct option { const char *name; /* 长选项的名称 */ int has_arg; /* 是否需要参数 */ int *flag; /* 如果用户提供了此选项，*flag 将被赋值为 @val */ int val; /* 将长选项与短选项绑定，或赋值给 @*flag */ }; 字段 has_arg 有两个有效值，即宏 no_argument 和 required_argument。\n如果字段 flag 未提供，则字段 val 会被视为与当前长选项绑定的短选项。\n如果提供了字段 flag，当用户提供当前长选项时，字段 val 将被赋值给 *flag。\n用实例详细说明 以下是一个使用 struct option 数组和模式字符串的实例：\n/* the corresponding pattern string */ const char *pattern = \u0026#34;suR:\u0026#34;; struct option long_options[] = { /* long option only */ /* with flag set - 1 */ { \u0026#34;onlyset1\u0026#34;, no_argument, \u0026amp;onlyset1, 1 }, /* long option only */ /* with flag set - 2 */ { \u0026#34;onlyset2\u0026#34;, no_argument, \u0026amp;onlyset1, 2 }, /* long option only */ /* with flag set - 3 */ { \u0026#34;onlyset3\u0026#34;, no_argument, \u0026amp;onlyset2, 3 }, /* long option only ( bind failed ) */ /* with flag set, with val bind */ { \u0026#34;setbind\u0026#34;, no_argument, \u0026amp;setbind_flag, \u0026#39;s\u0026#39; }, /* short and long option */ /* without flag set, with val bind */ { \u0026#34;unsetbind\u0026#34;, no_argument, NULL, \u0026#39;u\u0026#39; }, /* short and long option */ /* require argument */ { \u0026#34;RequireArg\u0026#34;, required_argument, NULL, \u0026#39;R\u0026#39; }, }; 考虑到短选项和长选项，当提到某个功能的option时，可能会有以下三种情况：\n仅有短选项 仅有长选项 同时具有短选项和长选项 结合模式字符串和 struct option 数组，程序接收的选项如下：\n短选项 长选项 \u0026ndash;onlyset1 \u0026ndash;onlyset2 \u0026ndash;onlyset3 \u0026ndash;setbind -s -u \u0026ndash;unsetbind -R \u0026ndash;RequireArg 选项 -s 和 --setbind 并未绑定在一起，因为在与 \u0026quot;setbind\u0026quot; 对应的结构体元素中设置了字段 flag。\n参数需求冲突 当短选项和长选项中都定义了参数需求时，可能会发生冲突。\n我们通过一个实例进行说明：\n/* the corresponding pattern string */ const char *pattern = \u0026#34;c:d\u0026#34;; struct option long_options = { /* short and long option */ /* conflict argument require - 1 */ { \u0026#34;Conflict1\u0026#34;, no_argument, NULL, \u0026#39;c\u0026#39; }, /* short and long option */ /* conflict argument require - 2 */ { \u0026#34;Conflict2\u0026#34;, required_argument, NULL, \u0026#39;d\u0026#39;}, /* end of the array ( all zero ) */ { 0, 0, 0, 0 } }; 我们可以看到，上述程序将一个需要参数的短选项 -c 与一个不需要参数的长选项 --Conflict1 绑定在一起。\n同时，它还将一个不需要参数的短选项 -d 与一个需要参数的长选项 --Conflict2 绑定在一起。\n该程序运行正常，但关于参数的配置也会生效。\n当用户提供 -c 或 --Conflict1 时，程序会进入同一个分支，但 --Conflict1 不会读取后续的参数，而 -c 会读取。\n当用户提供 -d 或 --Conflict2 时，结果则相反。\ngetopt_long() 示例程序 以下是使用 getopt_long() 的参数处理框架：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;getopt.h\u0026gt; /* test instructions: ./test_long.out --onlyset1 --onlyset2 --onlyset3 ./test_long.out -s --setbind ./test_long.out --unsetbind --RequireArg arg:RequireArg ./test_long.out -u -R arg:R ./test_long.out -c arg:c -d arg:d ./test_long.out --Conflict1 arg:Conflict1 --Conflict2 arg:Conflict2 ./test_long.out -c arg:c --Conflict1 arg:Conflict1 ./test_long.out --Conflict1 arg:Conflict1 -c arg:c */ int main(int argc, char *argv[]) { int opt; int onlyset1 = 0; int onlyset2 = 0; int setbind_flag = 0; struct option long_options[] = { /* long option only */ /* with flag set - 1 */ { \u0026#34;onlyset1\u0026#34;, no_argument, \u0026amp;onlyset1, 1 }, /* long option only */ /* with flag set - 2 */ { \u0026#34;onlyset2\u0026#34;, no_argument, \u0026amp;onlyset1, 2 }, /* long option only */ /* with flag set - 3 */ { \u0026#34;onlyset3\u0026#34;, no_argument, \u0026amp;onlyset2, 3 }, /* long option only ( bind failed ) */ /* with flag set, with val bound */ { \u0026#34;setbind\u0026#34;, no_argument, \u0026amp;setbind_flag, \u0026#39;s\u0026#39; }, /* short and long option */ /* without flag set, with val bound */ { \u0026#34;unsetbind\u0026#34;, no_argument, NULL, \u0026#39;u\u0026#39; }, /* short and long option */ /* require argument */ { \u0026#34;RequireArg\u0026#34;, required_argument, NULL, \u0026#39;R\u0026#39; }, /* short and long option */ /* conflict argument require - 1 */ { \u0026#34;Conflict1\u0026#34;, no_argument, NULL, \u0026#39;c\u0026#39; }, /* short and long option */ /* conflict argument require - 2 */ { \u0026#34;Conflict2\u0026#34;, required_argument, NULL, \u0026#39;d\u0026#39;}, /* end of the array ( all zero ) */ { 0, 0, 0, 0 } }; int long_index = 0; while ((opt = getopt_long(argc, argv, \u0026#34;suR:c:d\u0026#34;, long_options, \u0026amp;long_index)) != -1) { switch (opt) { case 0: /* provide long option with flag set */ printf(\u0026#34;long option with flag set recognized!\\n\u0026#34;); printf(\u0026#34;long option name is %s\\n\u0026#34;, long_options[long_index].name); if (long_options[long_index].flag != NULL) { printf(\u0026#34;the flag value is %d\\n\u0026#34;, *(long_options[long_index].flag)); } break; case \u0026#39;s\u0026#39;: /* provide option -s instead of --setbind */ printf(\u0026#34;long option (short option only), with flag set, with val bound.\\n\u0026#34;); printf(\u0026#34;setbind_flag == %d\\n\u0026#34;, setbind_flag); break; case \u0026#39;u\u0026#39;: /* provide option -u and --unsetbind */ printf(\u0026#34;long option, without flag set, with val bound.\\n\u0026#34;); break; case \u0026#39;R\u0026#39;: /* provide option -R and --RequireArg */ printf(\u0026#34;long option require argument.\\n\u0026#34;); printf(\u0026#34;the argument: %s\\n\u0026#34;, optarg); break; case \u0026#39;c\u0026#39;: /* -c require an argument while --Conflict1 not */ printf(\u0026#34;conflict argument require - 1\\n\u0026#34;); printf(\u0026#34;the argument: %s\\n\u0026#34;, optarg); break; case \u0026#39;d\u0026#39;: /* --Conflict2 require an argument while -d not */ printf(\u0026#34;conflict argument require - 2\\n\u0026#34;); printf(\u0026#34;the argument: %s\\n\u0026#34;, optarg); break; case \u0026#39;?\u0026#39;: /* Unrecognized option or missing argument */ printf(\u0026#34;Unknown option or missing argument.\\n\u0026#34;); break; } printf(\u0026#34;\\n\u0026#34;); } /* Process remaining non-option arguments */ for (int index = optind; index \u0026lt; argc; index++) printf(\u0026#34;Non-option argument %s\\n\u0026#34;, argv[index]); return 0; } 运行测试指令并分析测试结果 编译运行时的测试指令如下：\n./test_long.out --onlyset1 --onlyset2 --onlyset3 ./test_long.out -s --setbind ./test_long.out --unsetbind --RequireArg arg:RequireArg ./test_long.out -u -R arg:R ./test_long.out -c arg:c -d arg:d ./test_long.out --Conflict1 arg:Conflict1 --Conflict2 arg:Conflict2 ./test_long.out -c arg:c --Conflict1 arg:Conflict1 ./test_long.out --Conflict1 arg:Conflict1 -c arg:c 结果如下：\n\u0026gt; ./test_long.out --onlyset1 --onlyset2 --onlyset3 long option with flag set recognized! long option name is onlyset1 the flag value is 1 long option with flag set recognized! long option name is onlyset2 the flag value is 2 long option with flag set recognized! long option name is onlyset3 the flag value is 3 指令 ./test_long.out --onlyset1 --onlyset2 --onlyset3 展示了 getopt_long() 的基本功能。\n它改变了 flag 地址中的值。\n\u0026gt; ./test_long.out -s --setbind long option (short option only), with flag set, with val bound. setbind_flag == 0 long option with flag set recognized! long option name is setbind the flag value is 115 指令 ./test_long.out -s --setbind 展示了绑定失败（当 flag 字段被设置时）。\n--setbind 和 -s 会进入不同的分支。\n\u0026gt; ./test_long.out --unsetbind --RequireArg arg:RequireArg long option, without flag set, with val bound. long option require argument. the argument: arg:RequireArg \u0026gt; ./test_long.out -u -R arg:R long option, without flag set, with val bound. long option require argument. the argument: arg:R 这对比展示了绑定成功的结果（当 flag 字段未设置时）。\n--unsetbind 和 -u 被绑定在一起，进入相同的分支。\n--RequireArg 和 -R 也是一样。\n\u0026gt; ./test_long.out -c arg:c -d arg:d conflict argument require - 1 the argument: arg:c conflict argument require - 2 the argument: (null) Non-option argument arg:d \u0026gt; ./test_long.out --Conflict1 arg:Conflict1 --Conflict2 arg:Conflict2 conflict argument require - 1 the argument: (null) conflict argument require - 2 the argument: arg:Conflict2 Non-option argument arg:Conflict1 这对比展示了参数要求的冲突。\n-c 和 --Conflict1 被绑定在一起，但 -c 需要一个参数。\n-d 和 --Conflict2 被绑定在一起，但 --Conflict2 需要一个参数。\n结果显示，绑定在一起的选项会进入相同的分支。\n但是，是否读取或忽略参数取决于用户提供的选项是否需要参数。\n\u0026gt; ./test_long.out -c arg:c --Conflict1 arg:Conflict1 conflict argument require - 1 the argument: arg:c conflict argument require - 1 the argument: (null) Non-option argument arg:Conflict1 \u0026gt; ./test_long.out --Conflict1 arg:Conflict1 -c arg:c conflict argument require - 1 the argument: (null) conflict argument require - 1 the argument: arg:c Non-option argument arg:Conflict1 冲突选项的另一个对比结果。\n","date":"2025-01-22T13:29:03+08:00","permalink":"https://xingfend.github.io/blog-by-hugo/post/c-getopt_h-analysis/","title":"c语言getopt.h库解析"}]