可执行文件
进程
,其实就是可执行文件
在内存中加载得到的结果;
可执行文件
必须是操作系统可理解的格式,而且不同系统的可执行文件
的格式也是不同的;
不同平台的可执行文件
Linux:ELF
文件Windows
:PE32/PE32+
文件OS和iOS
:Mach-O(Mach Object)
文件
Mach-O 文件
Mach-O
文件是iOS
,iPadOS
、macOS
平台的可执行文件格式。对应系统通过应用二进制接口(application binary interface
,缩写为ABI
)来运行该格式的文件Mach-O
格式用来替代BSD
系统中的a.out
格式,保存了在编译和链接过程中产生的机器代码和数据
,从而为静态链接和动态链接的代码提供单一文件格式。Mach-O
提供了更强的扩展性,以及更快的符号表信息访问速度可执行文件
、目标文件
或者静态库
和动态库
等都是Mach-O
格式
Mach-o文件和进程的映像(image)
iOS系统生成的可执行程序或者动态库文件的存储布局格式被称之为mach-o格式。文件中存放着程序的代码和数据,而程序运行时系统会为其建立一个进程,以及分配虚拟内存空间。同时会把程序文件中的内容加载到虚拟内存地址空间中去
一般来说一个进程中image
的内容和内存布局结构会和程序文件的内容以及存储布局结构一致,image
的首地址是一个struct mach_header
的结构体指针。
image
中内容的排列布局和程序文件都是以段Segment
为单位进行排列的。但是有一些情况image
的内存布局和内容可能会和程序文件的内存布局和内容不一致:
image
中的数据段部分,因为数据段部分大多是可以被读写访问的,也就是说可以在运行时被修改,或者某些信息会进行rebase(重定向)
处理。因此数据段不能被进程之间共享,而是每个进程单独维护一份。
- 通常只有不可变的代码段部分才会是内存和文件中的内容保持一致,并且多进程共享。一个很常见的例子就是进程中加载的动态库和框架中的代码段部分通常都是所有进程共享
- 即使是代码段也有可能映像中的内容和程序文件中的内容不一致。有一些
image
中的某些段的内容会是系统中缓存的段,而不是程序文件对应的段。一个很有代表性的例子就是CoreLocation这个库,当这个库被加载时你就会发现其image
中的有一些代码段的内容其实是系统缓存的内容而不是程序文件中的内容
所以说程序文件和程序被加载后在内存中映像之间并不是一一对应的。程序文件和映像之间的关系就如程序和进程之间的关系是一样的。在程序运行后对其在进程中所有的mach-o数据结构的访问都是基于image映像
而不是基于程序文件的。
Slide机制
构建一个程序时为了方便计算和处理会为这个程序设定一个默认在内存中加载的基地址。这样在程序中所有涉及到地址存储的代码中的地址变量都是以这个基地址为标准的。
- 任何一个库或者可执行程序在构建时都会指定一个加载的基地址,但是却无法保证这个基地址的唯一性。和无法保证程序映像的地址区间不产生重叠。因此有可能出现多个库加载到内存时的重叠覆盖的情况。
- iOS系统为保证的应用安全采用了一种称之为ASLR(Address space layout randomization) 的技术。这种技术会使得每个程序或者库每次运行加载到内存中时的基地址都不是固定而是随机的,这种机制会增加黑客的破解难度。
一个程序或者库加载到内存时的真实的基地址和程序构建时指定的基地址是不一样的。系统会选择不重叠的区域进行加载,但是这样就会出现地址指针访问异常,因为这些地址值并不是真实内存中的地址值。
为了解决这个为题,在程序构建中的时候,会添加一条Load Command
命令LC_DYLD_INFO_ONLY
,这个命令会记录需要进行调整的位置
当程序被加载到内存的时候,dyld就会将需要调整的地址分别进行调整处理,转换成为真实的内存地址,这个过程也称之为基地址重定向rebase
假设程序构建时指定的基地址为A,程序中某处保存的一个函数指针地址为x,而程序被加载到内存时的真实基地址为B。也就是说真实的基地址和构建时的基地址的偏移差就是B-A。我们称这个偏移差值为Slide值。因此真实的地址x被调整后应该是: x + (B - A)了
一个程序在构建时的基地址值可以在程序的第一个名为__TEXT的代码段描述结构体struct segment_command
中的vmaddr
数据成员中获取,而程序被加载后的得到的映像的mach-o头部结构体struct mach_header
指针则是映像被加载的真实的基地址,因此:
映像的Slide值 = 映像的mach_header结构体指针 - 映像的第一个__TEXT代码段描述结构体struct segmeng_command
中的vmaddr数据成员的值。
我们也可以用过_dyld_get_image_vmaddr_slide(uint32_t image_index)
来获取到slide
Mach-O文件结构
MachO主要包含三个主要的部分:
Header
:头部,描述CPU
类型、文件类型、加载命令的条数大小等信息Load Commands
:加载命令,其条数和大小已经在header
中被提供Data
:数据段- 还有一些其他信息
Dynamic Loader Info
:动态库加载信息
Function Starts
:入口函数
Symbol Table
:符号表
Dynamic Symbol Table
: 动态库符号表
String Table
:字符串表
Mach Header(可执行文件的头部)
查看XNU
源码 我们可以看到 header
的定义
struct mach_header_64 {
uint32_t magic; /*系统内核用来判断是否是mach-o格式*/
cpu_type_t cputype; /* CPU架构类型,比如ARM */
cpu_subtype_t cpusubtype; /* CPU架构的具体类型,比如ARM64,ARMV7 */
uint32_t filetype; /*mach-o文件类型, 可执行文件、目标文件或者静态库和动态库 */
uint32_t ncmds; /* load commands 的条数 */
uint32_t sizeofcmds; /* load commands 的大小 */
uint32_t flags; /* 标志位,用来系统加载或者链接 */
uint32_t reserved; /* 保留字段,相比于32位多出来的字段 */
};
复制代码
一般我们可以通过一下方法来获取到header
头
void getMachHeader(void){
// 开辟空间
machHeaderArr = (JYMachHeaderArr *)malloc(sizeof(JYMachHeaderArr));
//_dyld_image_count 获取所有的image的数量
machHeaderArr->allocLength = _dyld_image_count();
// 获取第一个image的基址
// intptr_t base_addr = _dyld_get_image_vmaddr_slide(0);
//每个imaged的大小* 总数
machHeaderArr->array = (JYMachHeader *)malloc(sizeof(JYMachHeader) * machHeaderArr->allocLength);
for (uint32_t i = 0; i < machHeaderArr->allocLength; i++) {
JYMachHeader *machHeader = &machHeaderArr->array[i];
//获取image的头
machHeader->header = _dyld_get_image_header(i);
//获取image的名称
machHeader->name = _dyld_get_image_name(i);
//获取进程中单个image加载的Slide值
// Slide 代表默认在内存中加载的基地址
machHeader->slide = _dyld_get_image_vmaddr_slide(i);
}
}
复制代码
Load Commands
Load Commands
是加载命令的列表,用于描述Data
在二进制文件和虚拟内存中的布局信息;Load Commands
记录了很多信息,例如动态链接器的位置、程序的入口、依赖库的信息、代码的位置、符号表的位置等;Load commands
由内核定义,不同版本的command
数量不同,其条数和大小记录在header
中;Load commands
的type
是以LC_
为前缀常量,譬如LC_SEGMENT
、LC_SYMTAB
等;
// 其结构为这样
struct load_command {
uint32_t cmd; /* load command的类型 例如 LC_SYMTAB、LC_SEGMENT */
uint32_t cmdsize; /* 加载命令的大小 */
};
复制代码
load commands
的类别有很多
#define LC_REQ_DYLD 0x80000000
/* Constants for the cmd field of all load commands, the type */
#define LC_SEGMENT 0x1 /* segment of this file to be mapped */
#define LC_SYMTAB 0x2 /* link-edit stab symbol table info */
#define LC_SYMSEG 0x3 /* link-edit gdb symbol table info (obsolete) */
#define LC_THREAD 0x4 /* thread */
#define LC_UNIXTHREAD 0x5 /* unix thread (includes a stack) */
#define LC_LOADFVMLIB 0x6 /* load a specified fixed VM shared library */
#define LC_IDFVMLIB 0x7 /* fixed VM shared library identification */
#define LC_IDENT 0x8 /* object identification info (obsolete) */
#define LC_FVMFILE 0x9 /* fixed VM file inclusion (internal use) */
#define LC_PREPAGE 0xa /* prepage command (internal use) */
#define LC_DYSYMTAB 0xb /* dynamic link-edit symbol table info */
#define LC_LOAD_DYLIB 0xc /* load a dynamically linked shared library */
#define LC_ID_DYLIB 0xd /* dynamically linked shared lib ident */
#define LC_LOAD_DYLINKER 0xe /* load a dynamic linker */
#define LC_ID_DYLINKER 0xf /* dynamic linker identification */
#define LC_PREBOUND_DYLIB 0x10 /* modules prebound for a dynamically */
复制代码
而在我们iOS
当中,用得最多的就是LC_SEGMENT_64
,也就是segment_command_64
struct segment_command_64 { /* for 64-bit architectures */
uint32_t cmd; /* 标志加载命令类型为LC_SEGMENT_64 */
uint32_t cmdsize; /* 表示加载命令大小(还包括了紧跟其后的nsects个section的大小) */
char segname[16]; /* 16个字节的段名字 */
uint64_t vmaddr; /* 段的虚拟内存起始地址 */
uint64_t vmsize; /* 段的虚拟内存大小 */
uint64_t fileoff; /* 段在文件中的偏移量 */
uint64_t filesize; /* 段在文件中的大小 */
vm_prot_t maxprot; /* 段页面所需要的最高内存保护(4 = r,2 = w,1 = x) */
vm_prot_t initprot; /* 段页面初始的内存保护 */
uint32_t nsects; /* 段中section数量 */
uint32_t flags; /* 标志位 */
};
复制代码
在MachOView
当中,我们能看到其结构为
我们用得最多的也就是__LINKEDIT
以及LC_SYMTAB
、__TEXT
、__DATA_CONST
、__DATA
,这些和我们的函数、数据、类、属性都是息息相关
__PAGEZERO
:__PAGEZERO
所指代的内存并没有真正被分配(或占用)。首先,在文件在硬盘上时,PAGEZERO 的 filesize 为 0,即不分配硬盘空间。再者,mach-O 是被加载进入虚拟内存而不是物理内存,物理内存的使用需要通过 MMU 进行印射。因此, PAGEZERO 占用的虚拟内存在物理内存中并没有被分配。如果程序访问该段内存,肯定会直接被系统拦截,报出 BAD_ACCESS 的错误,感兴趣的可以看看这篇文章PAGEZERO的作用
-
__TEXT:代码段、只读数据段、其中有
__text
、__stubs
、__stub_helper
、__const
、_objc_methname
、__cstring
、__objc_methtype
、__cstring
、__objc_classnmae
、__objc_methtype
、__ustring
、等等就拿_objc_classnmae来说,在
Section64 Header(__objc_classname)
当中可以看到Address
为000000010000AD26
,Size
为0000000000000195
我们来到Section64(__TEXT,__objc_classname)
可以看到,其实地址就是我们上面的AD26,前面的0x100000000
是固定的,我们计算一下起始位置+Size = 0x10000AEBB
,这个地址却比下面这个表中最后一个是AEAB,是在这个范围内。
这里需要注意的是,如果我们在程序运行中去拿这些,则是需要计算内存地址的,因为有ASLR的存在,每个image加入到内存中是不一样的,就拿__LINKEDIT
来说
// 假设这是我们上面拿到的header
const struct mach_header * header = NULL;
struct segment_command_64 * seg_linkedit = NULL;
// 因为load_command的地址是和header是紧密连接的,header的地址下面就是
uintptr_t cur = (uintptr_t)(((struct mach_header_64*)header) + 1);
// 开始循环load Commands下的条目
for (uint32_t i = 0; i<header->ncmds; i++) {
// 当前条目
struct load_command * command = (struct load_command*)cur;
// 判断是否为 LC_SEGMENT_64
if (command->cmd == LC_SEGMENT_64) {
struct segment_command_64 * segment_command = (struct segment_command_64*)command;
//因为LC_SEGMENT_64有几种,这里我们需要拿得到是SEG_LINKEDIT
if (strcmp(segment_command->segname, SEG_LINKEDIT)) {
seg_linkedit = segment_command;
}
}
// 查看下一个command
cur = cur + command->cmdsize;
}
复制代码
Data
Data
中存储了实际的数据与代码,主要包含方法、符号表、动态符号表、动态库加载信息(重定向、符号绑定等)等;
Data
中的排布完全按照Load Command
中的描述;Data
由Segment
(段)和Section
(节)的方式来组成,通常,Data
拥有多个segment
,每个segment
可以有零到多个section
节;- 不同的
segment
都有一段虚拟地址
映射到进程的地址空间;
几乎所有的Mach-O
文件都包含3
个segment
- __TEXT:代码段,只读可执行,存储
函数的二进制代码(__text)
,常量字符串(__cstring)
,OC的类/方法名
等信息 - __DATA:数据段, 可读可写,存储
OC的字符串(__cfstring)
,以及运行时的元数据:class/protocol/method
,以及全局变量,静态变量等; - __LINKEDIT:只读,存储启动
App
需要的信息,如bind & rebase 的地址
、函数的名称和地址等信息;
在Data
区中,Section
占了很大的比例,而且在Mach-O
中集中体现在__TEXT
和__DATA
两段里。
Section64
被定义在loader.h
文件中,具体代码如下:
struct section_64 { /* for 64-bit architectures */
char sectname[16]; /* section的名称 */
char segname[16]; /* section所在的segment名称 */
uint64_t addr; /* 内存中的起始位置 */
uint64_t size; /* section的大小 */
uint32_t offset; /* section的文件偏移 */
uint32_t align; /* 字节大小对齐 */
uint32_t reloff; /* 重定位入口的文件偏移 */
uint32_t nreloc; /* 重定位入口数量 */
uint32_t flags; /* 标志,section的类型和属性*/
uint32_t reserved1; /* 保留(用于偏移量或索引) */
uint32_t reserved2; /* 保留(用于count或sizeof) */
uint32_t reserved3; /* 保留 */
};
复制代码
Image操作API
- 获取当前进程中加载的映像的数量
uint32_t _dyld_image_count(void)
复制代码
- 获取某个image的mach-o头部信息结构体指针
const struct mach_header* _dyld_get_image_header(uint32_t image_index)
复制代码
- 获取进程中某个image加载的Slide值
intptr_t _dyld_get_image_vmaddr_slide(uint32_t image_index)
复制代码
- 获取进程中某个image的名称
const char* _dyld_get_image_name(uint32_t image_index)
复制代码
- 注册image加载和卸载的回调通知函数
void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide))
void _dyld_register_func_for_remove_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide))
复制代码
如果你通过函数_dyld_register_func_for_add_image
注册了一个映像被加载时的回调函数时,那么每当后续一个新的映像被加载但未初始化前就会调用注册的回调函数,回调函数的两个入参分别表示加载的映像的头结构和对应的Slide值。如果在调用_dyld_register_func_for_add_image
时系统已经加载了某些映像,则会分别对这些加载完毕的每个映像调用注册的回调函数。
如果你通过函数_dyld_register_func_for_remove_image
注册了一个映像被卸载时的回调函数时,那么每当一个映像被卸载前都会调用注册的回调函数,回调函数的两个入参分别表示卸载的映像的头结构和对应的Slide值。
这两个函数的作用通常用来做程序加载映像的监控以及一些统计处理。
- 获取某个库连接时和运行时的版本
//获取库运行时的版本号 libraryName是不带路径和拓展名以及不带lib前缀的库,例如libc++.dylib
int32_t NSVersionOfRunTimeLibrary(const char* libraryName)
//获取库链接时的版本号
int32_t NSVersionOfLinkTimeLibrary(const char* libraryName)
复制代码
如果库存在,函数返回库对应的版本号,如果库不存在或者没有被加载或者没有被链接则返回-1
这两个函数的主要用来做一些库分析和运行监测等功能,比如可以检测某个库是否是一个在运行时被加载而不是显示链接进来的动态库。
- 获取当前进程可执行程序的路径文件名
int _NSGetExecutablePath(char* buf, uint32_t* bufsize)
char buf[256];
uint32_t bufsize = sizeof(buf)/sizeof(char);
_NSGetExecutablePath(buf, &bufsize);
复制代码
- 注册当前线程结束时的回调函数
void _tlv_atexit(void (*termFunc)(void* objAddr), void* objAddr)
复制代码
有时候我们想监控线程的结束事件,那么就可以用这个函数来实现。这个函数用来监控当前线程的结束,当线程结束或者终止时就会调用注册的回调函数,_tlv_atexit
函数有两个参数:第一个是一个回调函数指针,第二个是一个扩展参数,作为回调函数的入参来使用
Segment与Section操作API
- 获取进程中映像的某段中某个节的非Slide的数据指针和尺寸
//获取进程中可执行程序映像的某个段中某个节的数据指针和尺寸。
char *getsectdata(const char *segname, const char *sectname, unsigned long *size)
//获取进程加载的库的segname段和sectname节的数据指针和尺寸。
char *getsectdatafromFramework(const char *FrameworkName, const char *segname, const char *sectname, unsigned long *size);
复制代码
这两个函数其实就是返回对应的节描述信息结构struct section
中的addr和size两个数据成员的值。需要注意的是返回的地址值是没有加上Slide值的指针,因此当我们要在进程中访问真实的地址时需要加上对应的Slide值,下面就是一个实例代码:
//一般索引为1的都是可执行文件映像
intptr_t slide = _dyld_get_image_vmaddr_slide(1);
unsigned long size = 0;
char *paddr = getsectdata("__TEXT", "__text", &size);
char *prealaddr = paddr + slide; //这才是真实要访问的地址。
复制代码
- 获取段和节的边界信息
//获取当前进程可执行程序映像的最后一个段的数据后面的开始地址。
unsigned long get_end(void);
//获取当前进程可执行程序映像的第一个__TEXT段的__text节的数据后面的开始地址。
unsigned long get_etext(void);
//获取获取当前进程可执行程序映像的第一个_DATA段的__data节的数据后面的开始地址
unsigned long get_edata(void);
复制代码
- 获取进程中可执行程序映像的段描述信息
//获取进程中可执行程序映像的指定段名的段描述信息
const struct segment_command *getsegbyname(const char *segname)
//上面函数的64位版本
const struct segment_command_64 *getsegbyname(const char *segname)
复制代码
这两个函数返回进程中可执行程序映像的某个段的段描述信息。段描述信息是一个struct segment_command
或者struct segment_command_64
结构体。
比如下面代码返回进程中可执行程序映像代码段__TEXT的段信息。
const struct segment_command_64 *psegment = getsegbyname("__TEXT");
复制代码
- 获取进程中可执行程序映像的某个段中某个节的描述信息
//获取进程中可执行程序映像的某个段中某个节的描述信息。
const struct section_64 *getsectbyname(const char *segname, const char *sectname)
struct section_64 *psection = getsectbyname("__TEXT","__text");
复制代码
- 获取进程中映像的段的数据
//获取指定映像的指定段的数据
uint8_t *getsegmentdata(const struct mach_header_64 *mhp, const char *segname, unsigned long *size)
复制代码
函数返回进程内指定映像mhp中的段segname中内容的地址指针,而整个段的尺寸则返回到size所指的指针当中。这个函数的内部实现就是返回段描述信息结构struct segment_command
中的vmaddr数据成员的值加上映像mhp的slide值。而size中返回的就是段描述信息结构中的vmsize数据成员。
因为在前面讲过因为映像加载时的slide值的缘故,所以映像中的各种mach-o结构体中涉及到地址的数据成员的值都需要加上slide值才能得到映像在内存中的真实加载地址。
进程中每个映像中的第一个__TEXT段的数据的地址其实就是这个映像的mach_header头结构的地址。这是一个比较特殊的情况。
下面的代码演示的是获取进程中第0个索引位置映像的__DATA段的数据
struct mach_header_64 *mhp = _dyld_get_image_header(0);
unsigned long size = 0;
uint8_t *pdata = getsegmentdata(mhp, "__DATA", &size);
复制代码
- 获取mach-o文件中的某段中的某个节的数据指针和尺寸
//获取指定mach-o文件中的某个段中的某个节的数据指针和尺寸
char *getsectdatafromheader_64(const struct mach_header_64 *mhp, const char *segname, const char *sectname, uint64_t *size)
复制代码
这个函数返回64位系统中的某个mach-o文件中的某个段中某个节的数据指针和尺寸。这两个函数其实就是返回对应的节描述信息结构struct section中的addr值和size值。因为这两个函数是针对mach-o文件的,但是也可以用在对应的库映像中,当应用在库映像中时就要记得对返回的结果加上对应的slide值才是真实的节数据所对应的地址!
大家如果对MachO
感兴趣可以多去看看欧阳大哥的文章 ,一个很厉害的大佬,文章中有很多API
都是从他那边抄来的