善恶众相

  • 首页

  • 分类

  • 归档

代码的编译与执行

发表于 2019-09-03 更新于 2019-10-18 分类于 iOS
从代码的编译与执行的角度,去开始一门新语言的学习

对于计算机语言来讲,从编译及运行的角度来看,可以将其分为三类:

编译型语言

将代码通过编译链接等处理,生成可执行二进制文件;然后再将文件加载进内存去执行

优点:编译器一般会有预编译的过程对代码进行优化。由于运行时不需要编译,所以编译型语言的程序执行效率高。
缺点:编译之后如果需要修改就需要整个模块重新编译。编译的时候根据对应的运行环境生成机器码,所以需要根据运行的操作系统环境编译不同的可执行文件。
比如:C,C++,Objective-C

以Mac平台的以下Objective-C代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>

#define kUIWindow "UIWindow"
struct objc_object{

};

struct objc_class{

};

typedef struct objc_object *id;
typedef struct objc_class *Class;
typedef const char *SEL;

Class objc_getClass(const char *aClassName)
{
// ...
return NULL;
}
id objc_msgSend(Class cls, const char *op)
{
// ...
return NULL;
}

int main(int argc, const char * argv[]){
Class cls = (Class)objc_msgSend(objc_getClass(kUIWindow), "alloc");
printf("Hello, World!\n");
return 0;
}

编译链接过程

由于Objective-C完全是由C语言实现的,所以它的编译过程与C语言几乎完全相同,首先我们先来看一下编译链接生成二进制文件的过程如下图:

将以上源码命名为objc.c,根据上图从OC源码到生成可执行文件(Mach-O)过程,大致分为一下步骤:

1. 预处理(Pre-Processing)

gcc -E objc.c -o objc.i
执行结果生成了objc.i文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
……
struct __sFILEX;
# 120 "/Applications/Xcode6.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/usr/include/stdio.h" 3 4
typedef struct __sFILE {
unsigned char *_p;
int _r;
int _w;
short _flags;
short _file;
struct __sbuf _bf;
int _lbfsize;


void *_cookie;
int (*_close)(void *);
int (*_read) (void *, char *, int);
fpos_t (*_seek) (void *, fpos_t, int);
int (*_write)(void *, const char *, int);

……

int main(int argc, const char * argv[]) {

Class cls = objc_msgSend(objc_getClass("UIWindow"), "alloc");
printf("Hello, World!\n");
return 0;
}
……

我们发现,该过程其实是把 stdio.h文件中的内容插入到了objc.i文件中,并且将用到宏OBJC的地方都替换成它对应的“NSObject”,所以该阶段主要处理#ifdef、 #include和#define等命令。

tips:
预编译过程主要处理那些源代码中以#开始的预编译指令,主要处理规则如下:
将所有的#define删除,并且展开所有的宏定义;
处理所有条件编译指令,如#if,#ifdef等;
处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。该过程递归进行,及被包含的文件可能还包含其他文件。
删除所有的注释//和 /**/;
添加行号和文件标识,如#2 “hello.c” 2,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号信息;
保留所有的#pragma编译器指令,因为编译器须要使用它们;

2. 编译(Compiling)

gcc -S objc.i -o objc.s
执行结果生成objc.s文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
……
_main: ## @main
.cfi_startproc
## BB#0:
pushq %rbp
Ltmp2:
.cfi_def_cfa_offset 16
Ltmp3:
.cfi_offset %rbp, -16
movq %rsp, %rbp
Ltmp4:
.cfi_def_cfa_register %rbp
subq $32, %rsp
leaq L_.str(%rip), %rax
movl $0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
movq %rax, %rdi
callq _objc_getClass
leaq L_.str1(%rip), %rsi
movq %rax, %rdi
callq _objc_msgSend
leaq L_.str2(%rip), %rdi
movq %rax, -24(%rbp)
movb $0, %al
callq _printf
……

我们发现该阶段生成了汇编代码,但是在进行转换之前编译器会对代码规范性及语法做检查然后给我警告或错误,如果没有检测到错误那么就会继续生成汇编代码文件。

编译其实就是将一种语言转化为另一种语言的过程,这个过程极其复杂,大致步骤分为一下步骤:

3. 汇编(Assembling)

gcc -c objc.s -o objc.o
汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可。

4. 链接(Linking)

gcc objc.o -o objc
链接器ld将各个目标文件组装在一起,解决符号依赖,库依赖关系,并生成可执行目标文件(ELF文件结构)objc,此文件包含的也是二进制代码和数据,它可以被直接拷贝到内存中去运行,我们可以通过./objc命令执行该文件(程序)。

回过头我们再看objc.c中我们调用了“printf”函数,但是在预编译生成的objc.i文件中,我们只找到了该函数的声明并没有定义,那该函数在哪里实现的呢?在默认情况下,这些系统级的调用的实现会被放到libc.so.6的库文件中,链接要做的就是把该函数的实现链接到libc.so.6的库中,这样在执行文件的时候就能够找到实现并完成程序的运行。

tips:函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为”.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。
静态库即静态链接库(Windows 下的 .lib,Linux 和 Mac 下的 .a)。之所以叫做静态,是因为静态库在编译的时候会被直接拷贝一份,复制到目标程序里,这段代码在目标程序里就不会再改变了。
静态库的好处很明显,编译完成之后,库文件实际上就没有作用了。目标程序没有外部依赖,直接就可以运行。当然其缺点也很明显,就是会使用目标程序的体积增大。
动态库即动态链接库(Windows 下的 .dll,Linux 下的 .so,Mac 下的 .dylib/.tbd)。与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。
不需要拷贝到目标程序中,不会影响目标程序的体积,而且同一份库可以被多个程序使用(因为这个原因,动态库也被称作共享库)。同时,编译时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。动态库带来的问题主要是,动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境。如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行(Linux 下喜闻乐见的 lib not found 错误)。

加载执行过程

本着刨根问底的精神我们延伸思考一下,计算机从按下电源键那一刻到操作系统启动完成,如怎样的过程呢?其实操作系统是计算机启动后运行的第一个进程(launchd),它也是等待内核加载完必要的资源之后才开始运行的;而我们的应用的启动和消亡都是由操作系统去统一维护和管理的。

从按下电源键到加载内核

更详细的描述请参阅这个篇文章[计算机是如何启动的]
(http://liaoph.com/how-computers-boot-up)

自举处理器就是运行内核初始化代码的处理器,当自举处理器从真实模式切换到保护模式之后,第一个执行的函数就是位于i386_init.c文件中的vstart()函数,想要知道内核初始化过程就要对内核的结构有所了解:

Darwin是OS X及iOS的核心操作系统,它是有内核xnu及众多系统库(libSystem.B.dylib,libc.dylib)组成,而xnu内核又是由BSD(包括文件系统、网络核心、POSIX接口)及Mach微内核(包括调度器、IPC、虚拟内存管理)组成,所以内核的初始化过程我们可以理解为对以上这些核心组件的初始化过程,代码的执行流程也说明了这一点:

值得注意的是load_init_program( )函数就是负责初始化PID 为1 的进程launchd,至此我们的操作系统才开始进行启动和初始化,在此之后会在load_dylinker()中调起__dyld_start()方法,此时才正式的从内核态切换到了用户态,此后的流程就是连接器dyld去加载镜像(image)的过程,值得说明的一点是OC的runtime运行时系统就是在这个加载镜像过程中去完成构建的,等到镜像加载完毕,会返回main()函数的地址给dyldStartup.s中调用__dyld_start()处,最后跳到主程序的main函数去执行,此时才正式进入我们的应用代码去执行。

解释型语言:

在程序运行时去执行再去执行编译,编译完后直接加载进内存去执行,而不会生成可执行的二进制文件

优点:有较好的平台兼容性,在任何环境中都可以运行,前提是安装了解释器/虚拟机。修改代码的时候直接修改就可以快速部署,不用停机维护。
缺点:每次运行的时候都要解释一遍,性能上不如编译型语言。
比如:PHP,Python,JavaScript

拿PHP语言来说,它的编译执行过程可以用下图表示:

本着刨根问底儿的精神,在这里对PHP进行一下了解,PHP其实只是一个脚本解析器,你可以把它理解为一个普通的函数,输入是PHP脚本,输出是执行结果。在网络应用场景下,PHP并没有像Golang那样实现http网络库,而是实现了FastCGI协议,然后与web服务器配合实现了http的处理,web服务器来处理http请求,然后将解析的结果再通过FastCGI协议转发给处理程序,处理程序处理完成后将结果返回给web服务器,web服务器再返回给用户,这就是PHP的FastCGI运行模式,这种模式下通过FPM(FastCGI
PRocess Manager)来对各个请求进行管理的。

FPM启动过程的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int main(int argc, char *argv[])
{
...
fcgi_fd = fpm_run(&max_requests);
parent = 0;

//初始化fastcgi请求
request = fpm_init_request(fcgi_fd);

//worker进程将阻塞在这,等待请求
while (EXPECTED(fcgi_accept_request(request) >= 0)) {
SG(server_context) = (void *) request;
init_request_info();

//请求开始
if (UNEXPECTED(php_request_startup() == FAILURE)) {
...
}
...

fpm_request_executing();
//编译、执行PHP脚本
php_execute_script(&file_handle);
...
//请求结束
php_request_shutdown((void *) 0);
...
}
...
//worker进程退出
php_module_shutdown();
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int fpm_run(int *max_requests)
{
struct fpm_worker_pool_s *wp;
for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
//调用fpm_children_make() fork子进程
is_parent = fpm_children_create_initial(wp);

if (!is_parent) {
goto run_child;
}
}
//master进程将进入event循环,不再往下走
fpm_event_loop(0);

run_child: //只有worker进程会到这里

*max_requests = fpm_globals.max_requests;
return fpm_globals.listening_socket; //返回监听的套接字
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void fpm_event_loop(int err)
{
//创建一个io read的监听事件,这里监听的就是在fpm_init()阶段中通过socketpair()创建管道sp[0]
//当sp[0]可读时将回调fpm_got_signal()
fpm_event_set(&signal_fd_event,
fpm_signals_get_fd(),
FPM_EV_READ,
&fpm_got_signal,
NULL);
fpm_event_add(&signal_fd_event, 0);

//如果在php-fpm.conf配置了request_terminate_timeout则启动心跳检查
if (fpm_globals.heartbeat > 0) {
fpm_pctl_heartbeat(NULL, 0, NULL);
}
//定时触发进程管理
fpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL);

//进入事件循环,master进程将阻塞在此
while (1) {
...
//等待IO事件
ret = module->wait(fpm_event_queue_fd, timeout);
...
//检查定时器事件
...
}
}

FPM启动及运行过程总结如下图:

混合型语言:

首先通过编译器生成一种有别于机器码的中间语言,然后再通过虚拟机去解释执行

比如:Java,C#

Java语言的编译及执行流程如下图:

Java更详细的编译及执行流程如下图:

C#语言的编译及执行流程如下图:

C#更详细的编译及执行流程如下图:

Reference

https://baijiahao.baidu.com/s?id=1602418434992761288&wfr=spider&for=pc

[PHP7内核剖析]
(https://www.kancloud.cn/nickbai/php7)
[计算机是如何启动的]
(http://liaoph.com/how-computers-boot-up)
[MacOS X系统启动过程a]
(https://blog.csdn.net/dianshanglian/article/details/56485263)
[MacOS X系统启动过程b]
(http://www.ruanyifeng.com/blog/2013/02/booting.html)

从数据到声音
  • 文章目录
  • 站点概览
Zrongl

Zrongl

23 日志
3 分类
GitHub E-Mail
  1. 1. 编译型语言
    1. 1.1. 编译链接过程
      1. 1.1.1. 1. 预处理(Pre-Processing)
      2. 1.1.2. 2. 编译(Compiling)
      3. 1.1.3. 3. 汇编(Assembling)
      4. 1.1.4. 4. 链接(Linking)
    2. 1.2. 加载执行过程
      1. 1.2.1. 从按下电源键到加载内核
  2. 2. 解释型语言:
  3. 3. 混合型语言:
  4. 4. Reference
© 2019 Zrongl
不争无尤
|
主题 – NexT.Mist v7.3.0
0%