C, Java, TypeScript到底怎么运行的?

  最近突然想起来C语言源文件在不同平台下编译得到的二进制文件是不同的, 这个古典问题又让我联想到JVM虚拟机以及webassembly, 他们之间到底有什么关系, 又是怎么演变而来的呢?

  在一个技术交流群内,有朋友问:“linux下的bin文件是在大多数linux平台下都能运行的码?比如Ubuntu下的bin能放在redhat上运行吗?”当时简单回答了一下,主要考虑三方面,CPU架构(指令集)、动态库、系统调用。

      linux下bin目录下存放的是编译好的二进制文件,这些二进制文件多由C语言开发编译得到,涉及到C的编译,我们就需要从cpu架构、动态库和系统api三方面来考虑。

      首先从底层考虑。对于指令集系统,可以分为精简指令集系统CISC和复杂指令集系统RISC,每个指令集系统又可以分为多种cpu架构,如精简指令集中有POWER/PowerPC架构、MIPS架构、Alpha架构、 ARM架构,而复杂指令集有x86、x86-64架构。在编译过程中,编译器根据cpu架构,将源代码编译成当前架构下机器可识别的机器指令。因此,当我们从A架构的cpu机器上编译出二进制文件,在B架构的cpu上运行时,很可能因为B架构的cpu并不认识A架构下编译出的指令,导致我们的程序无法正常运行。

     当cpu架构相同时,我们就要从OS层面考虑这个问题。比如都是x86架构cpu的两台机器,我们分别安装了linux和windows操作系统,linux可执行文件有其特定的封装格式elf格式,而windows操作系统则封装为pe格式。这些二进制文件很多时候并不是一个完整的可执行文件,在运行时候,通常会调用系统中已经编译好的动态库文件,在linux中为.so文件,windows中为dll,显然两者的动态库文件有着很大不同。因此,在只有二进制文件的情况下,由于需要调用的动态库文件不同,程序常常很难夸平台运行。

静态库与动态库

我们通常把一些公用函数制作成函数库,供其它程序使用。

  函数库分为静态库和动态库两种。

  静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。

  动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在。

总的来说, 静态库就是嵌入目标代码中的拷贝, 体积通常比较小, 动态库是第三方包, 因为体积比较大, 运行时以链接的形式调用.

 

      当命令运行不依赖动态库时(如将静态库编译到整个命令中),又要考虑什么呢?这就需要我们从C语言编译原理的角度来认识这个问题。C编译的过程分为编译和链接两步,编译将源文件编译成目标文件,链接则是将目标文件、启动代码和库文件三者结合在一起。启动代码就是最终运行的程序和操作系统之间的一个接口,如dos和linux运行在相同cpu硬件的机器上,因为硬件相同,所以目标文件是相同的,由于我们使用静态库编译的,因此库文件也是相同的,但dos和linux要使用不同的启动代码,因此,从dos机器上编译的二进制文件,在linux机器上也很难运行。

其实启动代码是个很无聊的概念, 理论上cpu架构相同, 操作系统动态链接库相同, 可执行文件就应该一毛一样, 但是不知道出于什么动机, Windows下生成的是exe, Linux下是bin, 这就很难说了, 可能是某种安全机制或者什么阴谋论 ?

      最后,回到我们原来的问题,redhat和Ubuntu上的bin能通用吗?答:如果架构不同的cpu编译出的操作系统中的bin,很多时候会不能跨平台通用的,如64位cpu上编译的redhat系统中的bin,放在32位的Ubuntu中就不能运行。在硬件相同,os也相同的情况下,就需要比较.so动态库文件、启动文件是否相同,如果三者都一致,我们可以判断这类文件通常是可以跨平台运行的。      
 

Java虚拟机

java的一个非常重要的特点就是与平台的无关性。实现方法是JVM。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。而引入Java虚拟机后,Java语言在不同平台上运行时不需要重新编译。jvm屏蔽了与具体平台相关的信息,使得编译器只需生成在Java虚拟机上运行的目标二进制代码,就可以在多种平台上不加修改地运行。Jvm在执行字节码时,把字节码解释成具体平台上的机器指令执行。这就是Java的能够“一次编译,到处运行”的原因。

 

'compiled once, running everywhere' 是java的经典台词, 和C/C++不同, java在OS之上又建立了一层虚拟机, 将class文件转换成兼容本地机器和系统的可执行文件.

JVM是个伟大的设计, 但也有不足的地方: 首先是执行的时候多了一次转换的开销(比C多了一层), 其次, 维护并兼容众多CPU架构和操作系统的虚拟机, 还要给上层提供统一的接口, 这实在是一个很艰巨的任务.

 

webassembly

为了解决以上的问题, JVM是一种解决方案, 但是很显然jvm并没有带来太多的便利, 解释型语言是另一种比较主流的解决方案, 比如Python也是'一行代码, 处处运行' 因为在源代码层是无需考虑底层软硬件的. 从而解释执行语言实现了跨平台, 因为方便, 解释执行语言也是目前编程界的发展趋势.

这让我联想到了浏览器的发展趋势. 浏览器也是一种虚拟机, 是一种安全的sandbox, 但为什么前端代码还是以源码的形式(HTML,CSS,JS)给浏览器VM执行呢, 为什么不像JVM一样将编译后的js文件传到浏览器呢? 原因是以前浏览器VM五花八门, 有chromium虚拟机, 有Firefox虚拟机, 还有万恶的IE(插一条新闻, 微软最近将ie踢出浏览器家族, 给他的定义是'兼容解决方案'), 所以有众多虚拟机竞争的情况下很像java一样统一二进制接口.

但是2019年, chromium虚拟机的占有率已经超过70%, 各大厂商认识到标准统一的趋势已经不可挡, 于是webassembly崛起了. wasm就是如同jvm一样的虚拟机. WebAssembly代码运行在JS虚拟机的沙盒环境中,具有与JavaScript相同的安全策略,浏览器确保相同的源和权限策略。

关于wasm我还在研究阶段, 所以具体原理就不说了, 但值得一提的是有了webassembly, web界的未来是非常promising的. typescript编译到wasm的项目AssemblyScript也非常火. Deno最近也正式发布了, 还换了一波动态头像. 从未停止进步的web终于迎来了新的质变, 期待吗?

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐