[关闭]
@qidiandasheng 2020-07-20T10:18:40.000000Z 字数 5926 阅读 1821

可执行文件(链接、装载与库)

技术


编译语言和解释语言

源代码到可执行文件的步骤

预编译->编译->汇编->链接

什么是目标文件

目标文件从结构上讲就是编译后的可执行文件格式,只是还没有经过链接的过程。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。

目标文件就是源代码编译后但未进行链接的那些中间文件(Windows的.obj和Linux下的.o)

生成目标文件

以下流程是生成可执行文件的过程(可执行文件也是目标文件)。由多个目标文件链接而成。

Boy.h:

  1. #import <Foundation/Foundation.h>
  2. @interface Boy : NSObject
  3. - (void)say;
  4. @end

Boy.m

  1. #import “Boy.h”
  2. @implementation Boy
  3. - (void)say
  4. {
  5. NSLog(@“hi there again!\n”);
  6. }
  7. @end

main.m

  1. #import "Boy.h"
  2. int main(int argc, char * argv[]) {
  3. @autoreleasepool {
  4. Boy *boy = [[Boy alloc] init];
  5. [boy say];
  6. return 0;
  7. }
  8. }
  1. xcrun clang -c Boy.m
  2. xcrun clang -c main.m

将编译后的文件链接起来,这样就可以生成 a.out 可执行文件了。

  1. xcrun clang main.o Boy.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation

目标文件的内容

机器指令代码、数据。

屏幕快照 2019-11-29 下午2.44.58.png-381kB

从上图可以看出可执行文件在存储时(没有调入到内存前)分为头部(Header)、代码区(text)、数据区(data)和未初始化数据区(bss)四个部分。这是基本上的可执行文件的格式,但不同平台下的可执行文件格式会略有不同,下面列出了Linux下的ELF格式可执行文件和Mac/iOS下的Mach-O格式可执行文件。

段(section)

一个可执行文件包含多个段,也就是多个 section。可执行文件不同的部分将加载进不同的 section,并且每个 section 会转换进某个 segment 里。这个概念对于所有的可执行文件都是成立的。

头部(header)

指明了 CPU 架构、大小端序、文件类型、Load Commands 个数等一些基本信息,Headers 能帮助校验目标文件合法性和定位文件的运行环境

我们可以使用 otool 来观察可执行文件的头部 -- 规定了这个文件是什么,以及文件是如何被加载的。通过 -h 可以打印出头信息:

  1. % otool -v -h a.out
  2. Mach header
  3. magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
  4. MH_MAGIC_64 X86_64 ALL 0x00 EXECUTE 19 2424 NOUNDEFS DYLDLINK TWOLEVEL PIE

截屏2020-07-14 下午5.21.54.png-454.7kB

代码段(text)

存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。

代码区的指令包括操作码和操作对象(或对象地址引用)。如果是立即数(即是具体的数值),将直接包含在代码中,如果是局部数据,将在运行时在栈区分配空间,然后再引用该数据的地址,如果是未初始化数据区和数据区,在代码中同样将引用该数据的地址。

数据段(data)

该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。

例如:一个不在任何函数内声明(全局变量)。如下使得变量 count 根据其初始值被存储初始化数据区中:

  1. int count = 100;

例如:在任意位置定义静态变量方式,这声明了一个静态数据并初始化,如果在任意函数体外声明,则表示其为一个静态全局变量,如果在函数体内(局部),则表示其为一个局部静态变量。另外,如果在一个函数名前加上 static,则表示此函数只能再当前文件中被调用:

  1. static int num = 200;

未初始化数据段(bss)

存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。

例如,一个不在任何函数内声明的未初始化变量,将 sum 存储到未初始化数据区:

  1. long sum[1000];

ELF可执行文件

.text .data .rodata comment

屏幕快照 2019-11-29 下午4.08.03.png-397.8kB

Mach-O 可执行文件

格式

Mach-O是 mac 以及 iOS 上目标文件的格式。

headerLoad command.textdata_constdata

截屏2020-07-15 上午8.42.57.png-315.8kB

加载命令(load command)

加载命令规定了文件的逻辑结构和文件在虚拟内存中的布局。可以通过 -l 来查看加载命令。

  1. otool -v -l a.out | open -f

输出(截取部分):

  1. a.out:
  2. Load command 0
  3. cmd LC_SEGMENT_64
  4. cmdsize 72
  5. segname __PAGEZERO
  6. vmaddr 0x0000000000000000
  7. vmsize 0x0000000100000000
  8. fileoff 0
  9. filesize 0
  10. maxprot ---
  11. initprot ---
  12. nsects 0
  13. flags (none)
  14. Load command 1
  15. cmd LC_SEGMENT_64
  16. cmdsize 712
  17. segname __TEXT
  18. vmaddr 0x0000000100000000
  19. vmsize 0x0000000000001000
  20. fileoff 0
  21. filesize 4096
  22. maxprot r-x
  23. initprot r-x
  24. nsects 8
  25. flags (none)
  26. Section
  27. sectname __text
  28. segname __TEXT
  29. addr 0x0000000100000eb0
  30. size 0x0000000000000087
  31. offset 3760
  32. align 2^4 (16)
  33. reloff 0
  34. nreloc 0
  35. type S_REGULAR
  36. attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
  37. reserved1 0
  38. reserved2 0
  39. Section
  40. sectname __stubs
  41. segname __TEXT
  42. addr 0x0000000100000f38
  43. size 0x0000000000000018
  44. offset 3896
  45. align 2^1 (2)
  46. reloff 0
  47. nreloc 0
  48. type S_SYMBOL_STUBS
  49. attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
  50. reserved1 0 (index into indirect symbol table)
  51. reserved2 6 (size of stubs)

截屏2020-07-14 下午5.27.26.png-360.2kB

Load command 0

加载的segment是__PAGEZERO,它的大小为 4GB。这 4GB 并不是文件的真实大小,但是规定了进程地址空间的前4GB被映射为不可执行、不可写和不可读。
0x0000000100000000转十进制=4294967296=4GB

Load command 1

加载的segment是__TEXT,里面存在多个SectionSection中的offset表明它在文件中的偏移量。

数据段

Mach-O的数据段部分稍微有点不同。

基本的可执行文件:

Mach-O的可执行文件:

内存中数据的存储结构

目标文件存储结构和内存存储结构对照

内存中的数据其实就是从目标文件中读入的,相比于目标文件主要少了Header区,Header的作用就是为了告诉系统怎么把目标文件载入到内存中。

而内存中多的堆区和栈区主要是运行时产生的数据所分配的内存区块,目标文件是编译时产生的当然就不存在堆区和栈区了。

注:在可执行文件中存放的一般叫段(segment)对应内存中的区。

一般可执行文件对应的内存结构:
导出图片Sun Jul 05 2020 16_40_17 GMT+0800 (中国标准时间).png-159.2kB

Mach-O可执行文件对应的内存结构:
7271477-6826f45e95473767.png-24.8kB

编译器自动分配释放 ,存放函数的参数值,局部变量的值,非OC对象(基础数据类型)等。其操作方式类似于数据结构中的栈,内存地址连续向下增长。

OC对象,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,内存地址向上增长。

一般可执行文件数据区

未初始化数据区(bss segment)

数据区(data segment)

Mach-O可执行文件数据区

全局区/静态区(data segment)

包括两个部分:未初始化过 、初始化过。也就是说,(全局区/静态区)在内存中是放在一起的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域;

常量区(data_const segment)

常量(比如字符串常量);

代码段(code segment/text segment)

存放函数的二进制代码,代码区的内存是由系统控制。

编译过程

源代码->预编译->词法分析->语法分析->语义分析->中间语言生成->目标代码生成与优化->目标代码

具体过程可参考:iOS 编译 LLVM/Clang

链接器的作用

链接器的主要作用是为了给编译器生成的目标代码分配空间,确定他们的地址。比如目标代码中有变量定义在其他模块,那就需要在最终链接的时候才能确定运行时的绝对地址。

加载Mach-O文件

iOS 系统架构

iOS 系统是基于 ARM 架构的,大致可以分为四层:

下载.png-116.8kB

其中,用户体验层、应用框架层和核心框架层,属于用户态,是上层 App 的活动空间。Darwin 是用户态的下层支撑,是 iOS 系统的核心。

Darwin 的内核是 XNU,而 XNU 是在 UNIX 的基础上做了很多改进以及创新。

内核加载流程

可执行文件是由iOS 系统的内核 XNU加载的。

总体来说,XNU 加载就是为 Mach-O 创建一个新进程,建立虚拟内存空间,解析 Mach-O 文件,最后映射到内存空间。流程可以概括为:

  1. 内核XNU fork 新进程;

  2. 为 Mach-O 分配内存;

  3. 解析 Mach-O;

  4. 读取 Mach-O 头信息;

  5. 遍历 load command 信息,将 Mach-O 映射到内存;

  6. 启动 dyld。

上面流程是内核的处理流程,最后用户态 dyld 会对 Mach-O 文件做库加载和符号解析。

参考

程序员的自我修养—链接、装载与库

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注