@qidiandasheng
2020-07-20T10:18:40.000000Z
字数 5926
阅读 1821
技术
编译语言
可产生包含机器码的可执行文件的语言
解释语言
不可产生可执行文件的语言
Java
是一种介于编译语言和解释语言两者之间的语言。它需要经过编译,但编译的结果不是机器码,而是Java字节码(Java byte codes)。Java字节码与机器码在结构上很相似,但Java字节码可以在一种虚拟的计算机下被解释,即Java虚拟机(Java Virtual Machine,JVM)上。被编译的Java程序产生Java字节码,之后计算机模拟JVM对其进行解释。Java程序的运行可以不受限于机器与图形操作系统的类型,所以它具有平台无关性(platform-independent)。
预编译->编译->汇编->链接
目标文件从结构上讲就是编译后的可执行文件格式,只是还没有经过链接的过程。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。
目标文件就是源代码编译后但未进行链接的那些中间文件(Windows的.obj和Linux下的.o)
以下流程是生成可执行文件的过程(可执行文件也是目标文件)。由多个目标文件链接而成。
Boy.h:
#import <Foundation/Foundation.h>
@interface Boy : NSObject
- (void)say;
@end
Boy.m
#import “Boy.h”
@implementation Boy
- (void)say
{
NSLog(@“hi there again!\n”);
}
@end
main.m
#import "Boy.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
Boy *boy = [[Boy alloc] init];
[boy say];
return 0;
}
}
xcrun clang -c Boy.m
xcrun clang -c main.m
将编译后的文件链接起来,这样就可以生成 a.out
可执行文件了。
xcrun clang main.o Boy.o -Wl,`xcrun --show-sdk-path`/System/Library/Frameworks/Foundation.framework/Foundation
机器指令代码、数据。
从上图可以看出可执行文件在存储时(没有调入到内存前)分为头部(Header)、代码区(text)、数据区(data)和未初始化数据区(bss)四个部分。这是基本上的可执行文件的格式,但不同平台下的可执行文件格式会略有不同,下面列出了Linux下的ELF
格式可执行文件和Mac/iOS下的Mach-O
格式可执行文件。
一个可执行文件包含多个段,也就是多个 section
。可执行文件不同的部分将加载进不同的 section
,并且每个 section
会转换进某个 segment
里。这个概念对于所有的可执行文件都是成立的。
指明了 CPU 架构、大小端序、文件类型、Load Commands 个数等一些基本信息,Headers 能帮助校验目标文件合法性和定位文件的运行环境
我们可以使用 otool
来观察可执行文件的头部 -- 规定了这个文件是什么,以及文件是如何被加载的。通过 -h 可以打印出头信息:
% otool -v -h a.out
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL 0x00 EXECUTE 19 2424 NOUNDEFS DYLDLINK TWOLEVEL PIE
a.out
也是;Load command
(加载命令)的数量Load command
的size存放 CPU 执行的机器指令。通常代码区是可共享的(即另外的执行程序可以调用它),使其可共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。
代码区的指令包括操作码和操作对象(或对象地址引用)。如果是立即数(即是具体的数值),将直接包含在代码中,如果是局部数据,将在运行时在栈区分配空间,然后再引用该数据的地址,如果是未初始化数据区和数据区,在代码中同样将引用该数据的地址。
该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。
例如:一个不在任何函数内声明(全局变量)。如下使得变量 count 根据其初始值被存储初始化数据区中:
int count = 100;
例如:在任意位置定义静态变量方式,这声明了一个静态数据并初始化,如果在任意函数体外声明,则表示其为一个静态全局变量,如果在函数体内(局部),则表示其为一个局部静态变量。另外,如果在一个函数名前加上 static,则表示此函数只能再当前文件中被调用:
static int num = 200;
存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为 0 或者空(NULL)。
例如,一个不在任何函数内声明的未初始化变量,将 sum 存储到未初始化数据区:
long sum[1000];
.text
.data
.rodata
comment
Mach-O
是 mac 以及 iOS 上目标文件的格式。
header
、Load command
、.text
、data_const
、data
加载命令规定了文件的逻辑结构和文件在虚拟内存中的布局。可以通过 -l
来查看加载命令。
otool -v -l a.out | open -f
输出(截取部分):
a.out:
Load command 0
cmd LC_SEGMENT_64
cmdsize 72
segname __PAGEZERO
vmaddr 0x0000000000000000
vmsize 0x0000000100000000
fileoff 0
filesize 0
maxprot ---
initprot ---
nsects 0
flags (none)
Load command 1
cmd LC_SEGMENT_64
cmdsize 712
segname __TEXT
vmaddr 0x0000000100000000
vmsize 0x0000000000001000
fileoff 0
filesize 4096
maxprot r-x
initprot r-x
nsects 8
flags (none)
Section
sectname __text
segname __TEXT
addr 0x0000000100000eb0
size 0x0000000000000087
offset 3760
align 2^4 (16)
reloff 0
nreloc 0
type S_REGULAR
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
reserved1 0
reserved2 0
Section
sectname __stubs
segname __TEXT
addr 0x0000000100000f38
size 0x0000000000000018
offset 3896
align 2^1 (2)
reloff 0
nreloc 0
type S_SYMBOL_STUBS
attributes PURE_INSTRUCTIONS SOME_INSTRUCTIONS
reserved1 0 (index into indirect symbol table)
reserved2 6 (size of stubs)
Load command 0
加载的
segment
是__PAGEZERO,它的大小为 4GB。这 4GB 并不是文件的真实大小,但是规定了进程地址空间的前4GB被映射为不可执行、不可写和不可读。
0x0000000100000000转十进制=4294967296=4GB
Load command 1
加载的
segment
是__TEXT,里面存在多个Section
,Section
中的offset
表明它在文件中的偏移量。
Mach-O的数据段部分稍微有点不同。
基本的可执行文件:
Mach-O的可执行文件:
内存中的数据其实就是从目标文件中读入的,相比于目标文件主要少了Header
区,Header
的作用就是为了告诉系统怎么把目标文件载入到内存中。
而内存中多的堆区和栈区主要是运行时产生的数据所分配的内存区块,目标文件是编译时产生的当然就不存在堆区和栈区了。
注:在可执行文件中存放的一般叫段(segment)对应内存中的区。
一般可执行文件对应的内存结构:
Mach-O可执行文件对应的内存结构:
编译器自动分配释放 ,存放函数的参数值,局部变量的值,非OC对象(基础数据类型)等。其操作方式类似于数据结构中的栈,内存地址连续向下增长。
OC对象,一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,内存地址向上增长。
Block Started by Symbol
(未初始化数据段):并不给该段的数据分配空间,仅仅是记录了数据所需空间的大小。包括两个部分:未初始化过 、初始化过。也就是说,(全局区/静态区)在内存中是放在一起的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域;
常量(比如字符串常量);
存放函数的二进制代码,代码区的内存是由系统控制。
源代码->预编译->词法分析->语法分析->语义分析->中间语言生成->目标代码生成与优化->目标代码
具体过程可参考:iOS 编译 LLVM/Clang
链接器的主要作用是为了给编译器生成的目标代码分配空间,确定他们的地址。比如目标代码中有变量定义在其他模块,那就需要在最终链接的时候才能确定运行时的绝对地址。
iOS 系统是基于 ARM 架构的,大致可以分为四层:
其中,用户体验层、应用框架层和核心框架层,属于用户态,是上层 App 的活动空间。Darwin 是用户态的下层支撑,是 iOS 系统的核心。
Darwin 的内核是 XNU,而 XNU 是在 UNIX 的基础上做了很多改进以及创新。
可执行文件是由iOS 系统的内核 XNU加载的。
总体来说,XNU 加载就是为 Mach-O 创建一个新进程,建立虚拟内存空间,解析 Mach-O 文件,最后映射到内存空间。流程可以概括为:
内核XNU fork 新进程;
为 Mach-O 分配内存;
解析 Mach-O;
读取 Mach-O 头信息;
遍历 load command 信息,将 Mach-O 映射到内存;
启动 dyld。
上面流程是内核的处理流程,最后用户态 dyld 会对 Mach-O 文件做库加载和符号解析。
程序员的自我修养—链接、装载与库