Windows编程系列:PE文件结构

2024-08-31 23:52
文章标签 windows 系列 编程 结构 pe

本文主要是介绍Windows编程系列:PE文件结构,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Windows编程系列:PE文件结构

PE文件结构

Portable Executable (PE),可移植的可执行文件。在Windows平台下,所有的可执行文件(包括.exe, .dll, .sys, .ocx, .com等)均使用PE文件结构。这些使用了PE文件结构的可执行文件也称为PE文件。

PE结构包含的结构体有DOS头,PE标识 、文件头、可选头、目录头、目录结构、节表等
整体结构如下:
在这里插入图片描述

从上图可以看出PE结构分为4大部分,其中每个部分又进行了细分。

从数据管理的角度来看,可以把PE文件大致分为两部分,

1、DOS头、PE头和节表属于PE文件的数据管理结构或数据组织结构部分,

2、节表数据才是PE文件真正的数据部分,其中包含着代码、数据、资源等内容。

DOS头

DOS头分为“MZ头部”和"DOS存根“。

”MZ头部“是真正的DOS头部,由于其开始处的两个字节为"MZ",因此DOS头也可以叫作MZ头部。

这个我们用十六进制编辑器随便打开一个exe就可以看到
在这里插入图片描述
该部分用于程序在DOS系统下加载,它的结构被定义为IMAGE_DOS_HEADER

IMAGE_DOS_HEADER定义

//大小为: 0x40(64)字节#define IMAGE_DOS_SIGNATURE                 0x5A4D      // MZtypedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header+0h   WORD   e_magic;                     // MZ标记 0x5a4d+2h   WORD   e_cblp;                      // 最后(部分)页中的字节数+4h   WORD   e_cp;                        // 文件中的全部和部分页数+6H   WORD   e_crlc;                      // 重定位表中的指针数+8H   WORD   e_cparhdr;                   // 头部尺寸以段落为单位+0aH   WORD   e_minalloc;                  // 所需的最小附加段+0cH   WORD   e_maxalloc;                  // 所需的最大附加段+0eH   WORD   e_ss;                        // 初始的SS值(相对偏移量)+10H   WORD   e_sp;                        // 初始的SP值+12H   WORD   e_csum;                      // 补码校验值+14H   WORD   e_ip;                        // 初始的IP值+16H   WORD   e_cs;                        // 初始的SS值+18H   WORD   e_lfarlc;                    // 重定位表的字节偏移量+1aH   WORD   e_ovno;                      // 覆盖号+1cH   WORD   e_res[4];                    // 保留字+24H   WORD   e_oemid;                     // OEM标识符(相对m_oeminfo)+26H   WORD   e_oeminfo;                   // OEM信息+29H   WORD   e_res2[10];                  // 保留字+3CH   LONG   e_lfanew;                    // NT头(PE标记)相对于文件的偏移地址} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

DOS存根是一段简单的程序,主要用于输出“This program cannot be run in DOS mode.”类似的提示字符串。

为什么PE结构的最开始位置有这样一段DOS头部呢?

为了该可执行程序可以兼容DOS系统。通常情况下,Win32下的PE程序不能在DOS下运行,因此保留了这样一个简单的DOS程序用于提示“不能运行于DOS模式下”。

+3CH LONG e_lfanew; // NT头(PE标记)相对于文件的偏移地址
+3CH 开始的四个字节标识PE头开始
在这里插入图片描述

PE头解析

在这里插入图片描述

PE文件头由PE文件头标志,标准PE头,扩展PE头三部分组成。PE文件头标志自然是50 40 00 00,也就是’PE’,我们从结构体的角度看一下PE文件头的详细信息

typedef struct _IMAGE_NT_HEADERS {DWORD Signature; 						//PE文件头标志 => 4字节IMAGE_FILE_HEADER FileHeader; 			//标准PE头 => 20字节IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展PE头 => 32位下224字节(0xE0) 64位下240字节(0xF0)
} IMAGE_N

PE头签名(4个字节)

在这里插入图片描述

文件头IMAGE_FILE_HEADER(20字节)

IMAGE_FILE_HEADER是IMAGE_NT_HEADERS结构体中的一个结构,紧接在PE标识符(Signature字段)的后面。
IMAGE_FILE_HEADER占用20个字节
在这里插入图片描述

typedef struct _IMAGE_FILE_HEADER {WORD    Machine; 				//可以运行在什么平台上 任意:0 ,Intel 386以及后续:14C x64:8664WORD    NumberOfSections; 		//节的数量  区段数DWORD   TimeDateStamp; 			//编译器填写的时间戳,什么时间被创建DWORD   PointerToSymbolTable;   //调试相关DWORD   NumberOfSymbols; 		//调试相关WORD    SizeOfOptionalHeader;   //标识扩展PE头大小WORD    Characteristics;        //文件属性 => 16进制转换为2进制根据哪些位有1,可以查看相关属性
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine(1字节),标识在什么机器上运行

WORD 2字节,该字段表示可执行文件的目标CPU类型,取值如下:
在这里插入图片描述

这里我们看到的是64 86,也就是可以在上述对应环境运行在这里插入图片描述

NumberOfSections:WORD 2字节

该字段表示PE文件的节区的个数.请注意,Windows 加载程序将节区限制为 96。
该字段的值为06 00,即为0x00000006,表示该PE文件的节区有6个。
在这里插入图片描述
.txt 代码段
.data 可读写的数据段
.rdata 只读写的数据段
.pdata 节是由用于异常处理的函数表项组成的数组
.idata 导入数据段
.edata 导出数据段
.rsrc资源段
.reloc 主要用于存储基址重定位表
使用工具LordPE可以看到有6个节区
在这里插入图片描述

TimeDataStamp:

DWORD 4字节,该字段表示 文件是何时被创建的,这个值是自1970/1/1以来用格林威治时间计算的秒数。
在这里插入图片描述
界面上可以看到这个字段的取值是0xC2B75AE3,我们写一个简单的程序计算一下

import datetime# 给定的十六进制数
hex_seconds_since_epoch = 0xC2B75AE3# 将十六进制转换为十进制
decimal_seconds_since_epoch = int(hex_seconds_since_epoch)# 创建一个表示Unix纪元的datetime对象
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)# 从Unix纪元开始增加秒数
dt = epoch + datetime.timedelta(seconds=decimal_seconds_since_epoch)# 增加8小时来调整时区
dt_with_offset = dt.astimezone(tz=datetime.timezone(datetime.timedelta(hours=8)))# 打印结果
print(dt_with_offset.strftime('%Y-%m-%d %H:%M:%S %Z%z'))

在这里插入图片描述

PointerToSymbolTable:

DWORD 4字节,符号表的偏移量(以字节为单位),如果没有 COFF 符号表,则为零。
在这里插入图片描述

NumberOfSymbols

DWORD 4字节,符号表中的符号数。
在这里插入图片描述

SizeOfOptionalHeader

WORD 2字节,该字段 指定IMAGE_OPTIONAL_HEADER结构的大小。
在这里插入图片描述
我们这里的值是F0 00,也就是0x000000F0。

注意:在计算IMAGE_OPTIONAL_HEADER的大小时,应该取SizeOfOptionalHeader的值,而不是使用sizeof()函数。

因为IMAGE_OPTIONAL_HEADER结构体的大小可能是会改变的。

Characteristics

WORD 2字节,指定文件的类型。取值如下:
在这里插入图片描述

我们这里是22 00 ,也就是0x00000022,这是一个组合值(IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_LARGE_ADDRESS_AWARE),代表这是一个可执行文件,且能处理超过2GB的内存。
在这里插入图片描述

可选头IMAGE_OPTIONAL_HEADER

可选头IMAGE_OPTIONAL_HEADER是IMAGE_NT_HEADERS结构体中的一个结构,紧接在IMAGE_FILE_HEADER类型之后。

可选头是对文件头的一个补充。文件头主要描述文件的相关信息,而可选头主要用来管理PE文件被操作系统装载时所需要的信息。

IMAGE_OPTIONAL_HEADER在现在的大部分资料中都被称为可选头。但该头部实际上是一个必须存在的头。

所以这里应该是最初在翻译时或者其它方面导致的错误,后人一直沿用了。Option也有选项之类的意思 。

IMAGE_OPTIONAL_HEADER的大小在IMAGE_FILE_HEADER的SizeOfOptionalHeader字段中给出。

我们这里的值是F0 00,也就是IMAGE_OPTIONAL_HEADER的大小是0x000000F0。
在这里插入图片描述

IMAGE_OPTIONAL_HEADER(可选头)在IMAGE_FILE_HEADER(文件头)之后

IMAGE_FILE_HEADER的结束位置在0x000010F,那么可选头的起始位置是0x00000110

通过下面的图可以比较清晰的看到
在这里插入图片描述
选头的结束位置是0x00000110 + 0x000000F0 -1 = 0x000001FF,如下图所示
在这里插入图片描述通常情况下,可选头的结尾后面跟的是第一项节表的名称,就是值为.text的位置。
如上图所示:文件偏移0x0000200处的节点名称为".text",也就是说,可选头的结束位置在0x0000200偏移的前一字节,即0x00001FF
下面我们来详细看一下IMAGE_OPTIONAL_HEADER的定义

IMAGE_OPTIONAL_HEADER实际是一个宏,定义如下:

1 #ifdef _WIN64
2 typedef IMAGE_OPTIONAL_HEADER64             IMAGE_OPTIONAL_HEADER;
3 #else
4 typedef IMAGE_OPTIONAL_HEADER32             IMAGE_OPTIONAL_HEADER;
5 #endif

这里我们以x64为例,所以使用的是IMAGE_OPTIONAL_HEADER64类型,定义如下

typedef struct _IMAGE_OPTIONAL_HEADER64 {WORD        Magic;BYTE        MajorLinkerVersion;BYTE        MinorLinkerVersion;DWORD       SizeOfCode;DWORD       SizeOfInitializedData;DWORD       SizeOfUninitializedData;DWORD       AddressOfEntryPoint;DWORD       BaseOfCode;ULONGLONG   ImageBase;DWORD       SectionAlignment;DWORD       FileAlignment;WORD        MajorOperatingSystemVersion;WORD        MinorOperatingSystemVersion;WORD        MajorImageVersion;WORD        MinorImageVersion;WORD        MajorSubsystemVersion;WORD        MinorSubsystemVersion;DWORD       Win32VersionValue;DWORD       SizeOfImage;DWORD       SizeOfHeaders;DWORD       CheckSum;WORD        Subsystem;WORD        DllCharacteristics;ULONGLONG   SizeOfStackReserve;ULONGLONG   SizeOfStackCommit;ULONGLONG   SizeOfHeapReserve;ULONGLONG   SizeOfHeapCommit;DWORD       LoaderFlags;DWORD       NumberOfRvaAndSizes;IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

成员说明:

Magic

WORD 2字节,文件的状态类型,它可以是以下值之一
在这里插入图片描述
在这里插入图片描述
我们这里的值是0B 02,实际取值是0x0000020B,所以这是一个64位的可执行文件。

MinorLinkerVersion:链接器的次版本号

SizeOfCode:代码节的大小(以字节为单位),如果有多个代码块,则为所有此类块的总和。

SizeOfInitializedData:初始化的数据块的大小(以字节为单位),如果有多个已初始化的数据块,则为所有此类块的总和。

SizeOfUninitializedData:未初始化数据块的大小(以字节为单位),如果有多个未初始化的数据块,则为所有此类块的总和。

AddressOfEntryPoint:指向入口点函数(相对于映像基址)的指针。 对于可执行文件,这是起始地址。 对于设备驱动程序,这是初始化函数的地址。 入口点函数对于 DLL 是可选的。 如果没有入口点,则此成员为零。

BaseOfCode:指向代码部分开头(相对于映像基址)的指针。

ImageBase:文件被装入内存后的首选建议装载地址。对于EXE文件来说,通常情况下,该地址就是装载地址;对于DLL文件来说,可能就不是其装入内存后的地址了。

SectionAlignment:节表被装入内存后的对齐值。节表被映射到内存中需要对其的单位。在Win32下,通常情况下,该值为0x1000,也就是4KB大小。Windows操作系统的内存分页一般为4KB。此值必须大于或等于 FileAlignment 成员。

FileAlignment:节表在文件中的对齐值。通常情况下,该值为0x1000或0x200。在文件对齐值为0x1000时,由于与内存对齐值相同,可以加快装载速度。
而文件对齐值为0x200时,可以占用相对较少的磁盘空间。0x200是512字节,通常磁盘的一个扇区即为512字节。
说明:

程序无论是在内存中还是磁盘上,都无法恰好满足SectionAlignment和FileAlignment值的倍数,在不足的情况下需要补0值,这样就导致节与节之间存在了无用的空隙。这些空隙对于病毒之类程序而言就有了可利用的价值。
MajorOperatingSystemVersion:要求最低操作系统的主版本号。

MinorOperatingSystemVersion:要求最低操作系统的次版本号。

MajorImageVersion:可执行文件的主版本号。

MinorImageVersion:可执行文件的次版本号。

MajorSubsystemVersion:子系统的主版本号。

MinorSubsystemVersion:子系统的次要版本号。

Win32VersionValue:此成员为保留成员,必须为 0。

SizeOfImage:可执行文件装入内存后的总大小。该大小按内存对齐方式(SectionAlignment )对齐。

SizeOfHeaders:整个PE头部的大小。这个PE头部泛指DOS头、PE头、节表的总和大小。舍入为 FileAlignment 成员中指定的值的倍数。

CheckSum:校验和值。对于EXE文件通常为0;对于SYS文件,则必须有一个校验和。

SubSystem:运行此映像所需的子系统。 定义了以下值。
在这里插入图片描述
DllCharacteristics:指定DLL文件的特征,该值大部分时候为0,系统定义了以下值
在这里插入图片描述
SizeOfStackReserve:要为堆栈保留的字节数。 加载时只提交 由 SizeOfStackCommit 成员指定的内存;其余部分一次提供一页,直到达到此保留大小。

SizeOfStackCommit:要为堆栈提交的字节数。

SizeOfHeapReserve:要为本地堆保留的字节数。 加载时仅提交 由 SizeOfHeapCommit 成员指定的内存;其余部分一次提供一页,直到达到此保留大小。

SizeOfHeapCommit:要为本地堆提交的字节数。

LoaderFlags:此成员已过时。

NumberOfRvaAndSizes:数据目录项的个数。该个数在PSDK中有一个宏定义,

DataDirectory:数据目录表,由NumberOfRvaAndSizes个IMAGE_DATA_DIRECTORY结构体组成。该数组包含输入表、输出表、资源、重定位等数据目录项的RVA(相对虚拟地址)和大小。IMAGE_DATA_DIRECTORY结构体的定义如下:

1 typedef struct _IMAGE_DATA_DIRECTORY {
2     DWORD   VirtualAddress;
3     DWORD   Size;
4 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

成员说明:

VirtualAddress:该目录项的相对虚拟地址的起始值。

Size:目录项的长度。
数据目录中的成员在数据中的索引如下定义所示:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
//      IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor

在数据目录中,并不是所有的目录项都会有值很多目录项的值都为0。因为很多目录项的值为0,所以说数据目录项是可选的。

IMAGE_SECTION_HEADER

节表的位置在IMAGE_OPTIONAL_HEADER(可选头)后面。节表中的每个IMAGE_SECTION_HEADER中都存放着可执行文件被映射到内存中所在位置的信

息,节的个数由IMAGE_FILE_HEADER中的NumberOfSections给出。这个值我们在前面读取过,它是06 00,也就是0x00000006,所以该文件有6个节表。

IMAGE_SECTION_HEADER的大小为40字节,所以节表总共占用240个字节。

所以节表的起始位置在0x00000200,终止位置在0x00000200 + 0x000000F0(240) -1 = 0x000002EF处。如下图所示
在这里插入图片描述
IMAGE_SECTION_HEADER结构体的定义如下

typedef struct _IMAGE_SECTION_HEADER {BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];union {DWORD   PhysicalAddress;DWORD   VirtualSize;} Misc;DWORD   VirtualAddress;DWORD   SizeOfRawData;DWORD   PointerToRawData;DWORD   PointerToRelocations;DWORD   PointerToLinenumbers;WORD    NumberOfRelocations;WORD    NumberOfLinenumbers;DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Name(8字节)

节表项的名称,节的名称用ASCII编码来保存。节名称的长度为IMAGE_SIZE_SHORT_NAME(8个字节),这是一个宏
如果字符串长度正好为 8 个字符,则没有终止字符。否则最后一个字符为终止字符。 对于较长的名称,此成员包含 (/) 的正斜杠,后跟十进制数的 ASCII 表示形式,该数字是字符串表中的偏移量。 可执行文件不使用字符串表,并且不支持长度超过 8 个字符的节名称。

我们这里第一个节表项前面8个字节的数据为2E 74 65 78 74 00 00 00,对应的字符为 “.text”
在这里插入图片描述
Misc

Misc是一个联合体,它有以下两个成员

Misc.PhysicalAddress

文件地址。

Misc.VirtualSize

加载到内存中的节的总大小(以字节为单位)。 如果此值大于 SizeOfRawData 成员,则节将填充零。

此字段仅对可执行文件有效,对于对象文件,应设置为 0。

SizeOfRawData:该值为数据实际的节表项大小。 此值必须是 IMAGE_OPTIONAL_HEADER 结构的 FileAlignment 成员的倍数。 如果此值小于 VirtualSize 成员,则部分的其余部分将填充零。 如果节仅包含未初始化的数据,则成员为零。

PointerToRawData:该节表项在磁盘文件上的偏移地址。此值必须是 IMAGE_OPTIONAL_HEADER 结构的 FileAlignment 成员的倍数。 如果节仅包含未初始化的数据,请将此成员设置为零。

PointerToRelocations

A file pointer to the beginning of the relocation entries for the section. If there are no relocations, this value is zero.

这里的翻译总感觉差点意思 ,直接贴MSDN上的英文原版。

PointerToLinenumbers

A file pointer to the beginning of the line-number entries for the section. If there are no COFF line numbers, this value is zero.

NumberOfRelocations

该节表项重定向的条数。如果是可执行文件,该值应该设置为0

NumberOfLinenumbers

The number of line-number entries for the section.

Characteristics

节表项的属性,定义了以下值:
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

参考链接

  • https://www.cnblogs.com/zhaotianff/p/18186676

这篇关于Windows编程系列:PE文件结构的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1125355

相关文章

揭秘Python Socket网络编程的7种硬核用法

《揭秘PythonSocket网络编程的7种硬核用法》Socket不仅能做聊天室,还能干一大堆硬核操作,这篇文章就带大家看看Python网络编程的7种超实用玩法,感兴趣的小伙伴可以跟随小编一起... 目录1.端口扫描器:探测开放端口2.简易 HTTP 服务器:10 秒搭个网页3.局域网游戏:多人联机对战4.

Java并发编程必备之Synchronized关键字深入解析

《Java并发编程必备之Synchronized关键字深入解析》本文我们深入探索了Java中的Synchronized关键字,包括其互斥性和可重入性的特性,文章详细介绍了Synchronized的三种... 目录一、前言二、Synchronized关键字2.1 Synchronized的特性1. 互斥2.

使用Java实现通用树形结构构建工具类

《使用Java实现通用树形结构构建工具类》这篇文章主要为大家详细介绍了如何使用Java实现通用树形结构构建工具类,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录完整代码一、设计思想与核心功能二、核心实现原理1. 数据结构准备阶段2. 循环依赖检测算法3. 树形结构构建4. 搜索子

利用Python开发Markdown表格结构转换为Excel工具

《利用Python开发Markdown表格结构转换为Excel工具》在数据管理和文档编写过程中,我们经常使用Markdown来记录表格数据,但它没有Excel使用方便,所以本文将使用Python编写一... 目录1.完整代码2. 项目概述3. 代码解析3.1 依赖库3.2 GUI 设计3.3 解析 Mark

Python异步编程中asyncio.gather的并发控制详解

《Python异步编程中asyncio.gather的并发控制详解》在Python异步编程生态中,asyncio.gather是并发任务调度的核心工具,本文将通过实际场景和代码示例,展示如何结合信号量... 目录一、asyncio.gather的原始行为解析二、信号量控制法:给并发装上"节流阀"三、进阶控制

Windows Server服务器上配置FileZilla后,FTP连接不上?

《WindowsServer服务器上配置FileZilla后,FTP连接不上?》WindowsServer服务器上配置FileZilla后,FTP连接错误和操作超时的问题,应该如何解决?首先,通过... 目录在Windohttp://www.chinasem.cnws防火墙开启的情况下,遇到的错误如下:无法与

Python解析器安装指南分享(Mac/Windows/Linux)

《Python解析器安装指南分享(Mac/Windows/Linux)》:本文主要介绍Python解析器安装指南(Mac/Windows/Linux),具有很好的参考价值,希望对大家有所帮助,如有... 目NMNkN录1js. 安装包下载1.1 python 下载官网2.核心安装方式3. MACOS 系统安

Windows系统下如何查找JDK的安装路径

《Windows系统下如何查找JDK的安装路径》:本文主要介绍Windows系统下如何查找JDK的安装路径,文中介绍了三种方法,分别是通过命令行检查、使用verbose选项查找jre目录、以及查看... 目录一、确认是否安装了JDK二、查找路径三、另外一种方式如果很久之前安装了JDK,或者在别人的电脑上,想

Windows命令之tasklist命令用法详解(Windows查看进程)

《Windows命令之tasklist命令用法详解(Windows查看进程)》tasklist命令显示本地计算机或远程计算机上当前正在运行的进程列表,命令结合筛选器一起使用,可以按照我们的需求进行过滤... 目录命令帮助1、基本使用2、执行原理2.1、tasklist命令无法使用3、筛选器3.1、根据PID

Python中Windows和macOS文件路径格式不一致的解决方法

《Python中Windows和macOS文件路径格式不一致的解决方法》在Python中,Windows和macOS的文件路径字符串格式不一致主要体现在路径分隔符上,这种差异可能导致跨平台代码在处理文... 目录方法 1:使用 os.path 模块方法 2:使用 pathlib 模块(推荐)方法 3:统一使