前言
📫作者简介:小明java问道之路,专注于研究计算机底层/Java/Liunx 内核,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计📫
🏆InfoQ签约博主、CSDN专家博主/Java领域优质创作者、阿里云专家/签约博主、华为云专家、51CTO专家🏆
🔥如果此文还不错的话,还请👍关注 、点赞 、收藏三连支持👍一下博主~
本文导读
本文导读
精通真正的高并发编程,不仅仅是API的使用和原理!计算机最基础的程序是怎么组成的呢?这个都不知道,又如何能证明你的程序是高并发的?本文深入浅出,讲解程序的本质(编译的过程)、组成(程序所需的内存)与格式(ELF) ,希望读者可以构建计算机从写代码到编译到执行的链路的底层思维。
一、计算机程序的组成
1、程序执行的本质
我们先了解下我们平时写的程序都怎么写?怎么执行的?Java、Python、GO、C/C++等等语言,我们需要通过IDE(Integrated Development Environment )中编写,Java有JDK、各个语言都有自己的集成开发环境,然后在idea或者eclipse等等开发工具,通过集成开发环境IDE中的编译器或者内置编译器,变成CPU能工作执行的格式来执行。
举一个简单的C语言的小例子,printf(''); 是操作硬件打印字符,由于只有OS才能操作硬件,所以这个函数调用,一定调用了系统调用的接口,由于系统调用的接口约定较为复杂,且每个OS都不一样,而C语言需要可移植性,所以推理得出 有个东西包装了系统调用过程,提供统一接口,给应用程序使用,那么其就会调用GLIBC 的函数库函数。
#include <stdio.h>
int main() // 封装了汇编的指令片段调用过程:保存返回地址+开辟栈帧+传递参数+返回值
{ // 定义作用域:标识指令片段
printf(''); // 封装 call 指令和参数传递过程(寄存器传递,栈传递)
return 0; // 将返回值放入约定的地方
}
<stdio.h> 等价于 java(其他语言的)import ,将函数定义导入,为什么导入?因为编译器需要这些东西,虽然不知道具体的函数地址、变量地址在哪里,但是知道调用的什么东西,编译器才能对其记录,并且在某个时候将它所需要的东西给出。
2、程序保存在哪里
上面说的,这些编译好的数据保存在我们的电脑上,具体在我们的计算机哪里,我们需要了解计算机有哪些存储器,存储器系统(memory system)是一个需要考虑多元因素存储设备的层次结构,例如容量、成本和访问时间。
这些编译好的数据就保存在磁盘上(local disks) ,然后我们用过系统调用告诉操作系统(OS),由操作系统来获取编译好的数据并进行解析,将这些数据从磁盘加载到内存(DRAM) 。
然后在内存中创建一个进程来代表你写的这个程序,最后CPU执行。在这个过程中最重要的两个方面,一方面是上一小结的编译原理,另一方面就是这些编译好的数据是如何存储的,以及他们的格式是什么样的,这个格式就是操作系统(下属说操作系统都是Linux)如何正确的解释汇编代码。
3、计算机程序的组成
计算机程序保存的位置我们知道了,程序在内存的细节就需要了解了,程序组成不是凭空而来的,我们在了解编译及编译器和计算机内存体系之后,看看程序应该由哪几部分组成
栈区(stack,用于存储函数的参数值、局部变量值等)编译器可以自动分配和释放。
堆区(heap)由程序员进行分配和释放(一些编译器中也会自动管理,例如JVM的GC),若程序员不释放,程序结束由OS进行回收。
可执行程序包括BSS段、数据段、代码段:
数据段(data段),初始化的全局变量和静态变量的区域,程序结束由OS释放。
BSS段(Block Started bySymbol以符号开始的块,BSS是Linux 链接器产生的未初始化数据段),未初试化的全局变量和静态变量的区域,只记录需要的内存大小并不实际存放数据,同样结束由OS释放。
代码段(text 段)存储程序的二进制代码和场景等。
一个进程在运行过程中,代码是根据方法依次执行的,当然跳转和递归有可能使代码执行多次,而数据一般都需要访问多次,因此需要单独开辟空间以方便访问和节约空间。
临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。全局数据和静态数据有可能在整个程序执行过程中都需要访问,因此单独存储管理。堆区由用户自由分配,以便管理。
4、程序的格式(ELF)
ELF(Executable and Linkable Format,可执行链接的格式),这里的格式说的就是程序的格式,程序的格式分为3类:
可执行文件,文件保存着一个用来执行的程序(如bash、gcc等)。
可重定向文件,文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件(目标文件或者静态库文件,即 Linux 通常后缀为 .a和 .o的文件)。
共享目标文件,共享库文件保存着代码和合适的数据,用来被下连接编辑器和动态链接器链接(Linux中后缀为 .so的文件)。
本节用一个简单的例子,用C语言生成一个可执行文件,然后根据这个可执行文件分析和理解ELF格式下的 可执行文件的组成
#include <stdio.h>
int main() // 封装了汇编的指令片段调用过程:保存返回地址+开辟栈帧+传递参数+返回值
{ // 定义作用域:标识指令片段
printf(''); // 封装call指令和参数传递过程(寄存器传递,栈传递)
return 0; // 将返回值放入约定的地方
}
用 gcc demo.c 命令执行后,将会得到一个 a.out 的文件,这个文件就是 ELF文件可执行文件,当我们运行 ./.a,out 文件后,将会在控制台打印。
现在就通过这个得到的 a.out 文件来分析 ELF 文件的格式。这段代码的第一行 #include <stdio.h>,引入它是为了调用 printf 函数,底层通过调用 OS 提供的函数,向控制台打印,这不是我们自己实现的,这个步骤需要依赖C语言函数库 Glibc,而 stdio 就是含数据中包含的基本输入输出的定义。
但是我们编译后,可以看到并没有 Glibc->printf 的实现文件,那这个文件在哪里呢?
就在共享目标文件(共享链接库)中,并以 .so 结尾,在 Linux 中真正执行文件的输出操作的代码,已经在文件中了,接下来如何调用,这里面就涉及动态链接的知识