编译过程分为四个阶段:预处理、编译、汇编、链接
gcc -E hello.c -o hello.i //预处理
gcc -S hello.i -o hello.s //编译
gcc -c hello.s -o hello.o //汇编
gcc hello.o -o hello //生成可执行文件
以hello.c
为例子:
#include <stdio.h>
#define ANSWER 42
int main() {
int obj = ANSWER;
return 0;
}
预编译实际上是进行头文件、宏定义的替换和组织,执行上述预编译命令可查看其内容(展示部分结果):
# 1 "hello.c"
# 1 "<built-in>"
# 1 "<command-line>"
//...省略
# 216 "/usr/lib/gcc/x86_64-redhat-linux/8/include/stddef.h" 3 4
typedef long unsigned int size_t;
# 34 "/usr/include/stdio.h" 2 3 4
//...省略
typedef __builtin_va_list __gnuc_va_list;
# 37 "/usr/include/stdio.h" 2 3 4
...
# 28 "/usr/include/bits/types.h" 2 3 4
typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
//...省略
typedef int __sig_atomic_t;
# 39 "/usr/include/stdio.h" 2 3 4
# 1 "/usr/include/bits/types/__fpos_t.h" 1 3 4
# 1 "/usr/include/bits/types/__mbstate_t.h" 1 3 4
# 13 "/usr/include/bits/types/__mbstate_t.h" 3 4
typedef struct
{
int __count;
union
{
unsigned int __wch;
char __wchb[4];
} __value;
} __mbstate_t;
# 6 "/usr/include/bits/types/__fpos_t.h" 2 3 4
//...省略
extern void funlockfile (FILE *__stream) __attribute__ ((__nothrow__ , __leaf__));
# 864 "/usr/include/stdio.h" 3 4
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 879 "/usr/include/stdio.h" 3 4
# 2 "hello.c" 2
# 3 "hello.c"
int main() {
int obj = 42;
return 0;
}
我们发现预编译生成的.i
文件中已经不存在宏ANSWER
了,其值被替换成42
。编译阶段主要是对代码进行词法分析、语法分析、语义分析、中间代码优化等等。
然后是通过gcc -S
选项进行编译,编译生成的.s
是汇编程序,结果如下:
.file "hello.c"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movl $42, -4(%rbp)
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (GNU) 8.4.1 20200928 (Red Hat 8.4.1-1)"
.section .note.GNU-stack,"",@progbits
接着就是汇编,将汇编程序转换为ELF(Executable and Linkable Format)
格式的目标.o
程序,可通过gcc -c
的方式,或直接调用as
进行汇编:as -c hello.s -o hello.o
。
当拿到.o
文件后就可以进行链接或者直接生成可执行程序,链接的话需要加载链接库,链接库有动态链接库和静态链接库。以上就是代码编译系统的过程。
那为什么要理解一个程序的编译过程呢?要理解编译系统的原因:
- 理解编译系统可以优化程序的性能
- 理解链接时出现的错误
- 避免安全漏洞
接下来站在全局的角度大致了解硬件架构图,以便于我们了解程序执行的流程:
其中CPU(Central Processing Unit)中央处理单元包括:PC(Program Count)、寄存器堆(Register file)、ALU(Arithmatic/logic Unit)三部分。
那么执行一个hello程序,计算机内部会发生什么呢?(通常用户是在shell终端上执行代码的)
上图就是程序执行流程,概述如下:
- IO设备键盘输入字符串"./hello",shell程序将输入字符读入寄存器,处理器会把
hello
字符通过系统总线和内存总线加载至内存(此时还没有按下回车)。 - 当按下回车执行时,shell就知道我们已经完成命令输入,然后shell会执行一系列指令加载可执行文件hello。
- 加载的过程实际上是将代码所需要的数据从磁盘拷贝至内存。拷贝的过程被称为DMA(Direct Memory Access),DMA技术可将数据不经过处理器,从磁盘直接加载至内存,我们知道CPU时间是非常宝贵的,磁盘读取本身就很慢,通过DMA技术,拷贝操作不经过处理器,这样就不会剥夺占用CPU时间,达到更加高效的作用(对计算密集型主机来说特别有帮助)。
- 当可执行文件hello中的代码和数据被加载至内存中后,处理器就开始执行函数代码,那么一个程序运行起来就变为了进程。
那么上面说到了shell程序,其实更准确的说法应该是shell进程,进程是正在运行中的程序,当用户在shell中运行hello进程的时候,其实shell会发生中断,系统会保存其上下文信息,然后转而运行hello进程,当hello进程运行完毕后,又恢复shell进程上下文信息。大致形势如下:
在计算机系统中,每个进程都对应了4GB的虚拟内存地址,操作系统将实际硬件上的物理地址通过内存映射方式,将物理地址映射为连续大小4GB的虚拟内存地址空间,这4GB空间中,由低地址向高地址的3GB空间划分为用户空间,然后高地址部分的1GB划分为内核空间,专门用于保存系统级别数据信息。那么用户对一个程序的操作实际上是在用户空间进行的,划分如下:
关于系统加速,有以下三种定律:
-
阿姆达尔定律
-
古斯塔法森定律
-
孙-倪定律
三种模型关系:
关于并发、并行,首先什么是并发?什么是并行?
并行(parallelise
)同时刻(某点),即并行是在某一时刻上有多个任务在执行。
并发(concurrency
)同时间(某段),即并发是在某一时段内有多个任务在执行。
如何获得更高的计算力:
- 线程级并发:增加CPU核心数提高系统行呢哥
- 指令级并行:流水指令集
- 单指令多数据并行:SIMD指令加速