专栏首页皮皮鲁的AI星球计算机基础系列:源代码如何被计算机执行

计算机基础系列:源代码如何被计算机执行

计算机芯片的物理特性决定了它只能接受二进制指令。不同计算机芯片的指令集不同。高级编程语言需要转化成二进制机器语言才能被计算机所执行。编译型语言需要使用编译器经过编译和连接生成可执行文件,解释型语言需要使用解释器解释源代码。解释型语言更容易上手,但是运行速度更慢,必要时要使用C/C++重写或使用JIT技术加速。

现在各行各业的朋友都开始使用计算机解决自己的业务问题,网络上有大量的免费公开课,教我们处理数据并数学建模。Python等编程语言上手快,开源软件多,足以应付绝大多数的需求。在计算机软硬件体系中,上述工作都是在最顶层,用户执行程序需要依赖于计算机硬件和系统软件。聊天用的微信、娱乐玩的农药、上网打开的浏览器、还有我们自己写的程序…这些程序是如何从源代码,变成计算机芯片可以执行的程序呢?

计算机软硬件体系结构

本文并不是想把这个复杂的链条都解释得非常清楚,或者鼓励大家都去学习底层编程知识,而是想让读者对这个流程有一个基本的认识。因为,当我们熟练掌握了一定编程基础,开始上手更大规模的数据或更复杂的数学模型时,会遇到一些瓶颈,直接调用别人写好的程序或者应用新热算法都无法直接解决问题。这时候了解计算机的基本组成和运行原理,打好基础,才能为顶层应用拓展思维。

计算机只能执行二进制代码

也许你已经知道,计算机是基于二进制运行的。就像道家哲学的阴阳一样,计算机只有两个状态,开或关、真或假、1或0…因为,组成计算机的基本元件——半导体只能以二进制进行计算。我们编程所用的C/C++、Python、大数据、AI等层出不穷的技术,以及我们存储在电子设备的文本、音频、图像、视频等媒介,最终都是以二进制的形式,被计算和处理的。计算机体系最底层的工程师要使用二进制代码控制芯片来做计算和处理。

我在我的Mac上编写了一个名为plusc = a + b程序,其二进制和汇编代码如下所示:

➜  objdump -s plus

plus:   file format Mach-O 64-bit x86-64

_main:
100000f30:      55      pushq   %rbp
100000f31:      48 89 e5        movq    %rsp, %rbp
100000f34:      48 83 ec 20     subq    $32, %rsp
100000f38:      c7 45 fc 00 00 00 00    movl    $0, -4(%rbp)
100000f3f:      c7 45 f8 01 00 00 00    movl    $1, -8(%rbp)
100000f46:      c7 45 f4 02 00 00 00    movl    $2, -12(%rbp)
100000f4d:      c7 45 f0 00 00 00 00    movl    $0, -16(%rbp)
100000f54:      8b 45 f8        movl    -8(%rbp), %eax
100000f57:      03 45 f4        addl    -12(%rbp), %eax
100000f5a:      89 45 f0        movl    %eax, -16(%rbp)
100000f5d:      8b 75 f8        movl    -8(%rbp), %esi
100000f60:      8b 55 f4        movl    -12(%rbp), %edx
100000f63:      8b 4d f0        movl    -16(%rbp), %ecx
100000f66:      48 8d 3d 35 00 00 00    leaq    53(%rip), %rdi
100000f6d:      b0 00   movb    $0, %al
100000f6f:      e8 0e 00 00 00  callq   14 <dyld_stub_binder+0x100000f82>
100000f74:      31 c9   xorl    %ecx, %ecx
100000f76:      89 45 ec        movl    %eax, -20(%rbp)
100000f79:      89 c8   movl    %ecx, %eax
100000f7b:      48 83 c4 20     addq    $32, %rsp
100000f7f:      5d      popq    %rbp
100000f80:      c3      retq
........

首行的file format Mach-O 64-bit x86-64表示这是一个可以运行在64位x86架构的处理器上、基于Mac OS的一段程序。不同的计算机芯片厂商所设计的半导体电路不同,在芯片上编程的二进制规则不同。执行同样的一段c = a + b的逻辑,在基于ARM架构芯片的Android手机上所需要的二进制代码与上面展示的会截然不同。当前市场上计算机CPU芯片基本被几大科技公司垄断,除了刚提到的Intel和AMD研发的应用在个人电脑上的x86-64处理器,应用在手机、平板电脑等移动设备上的ARM架构处理器,还有应用在大型服务器和超级计算机上的IBM Power系列处理器等。不同架构的CPU处理器都有自己的一套指令集(instruction set architecture,简称ISA),这就像一个设计图纸和使用说明书,告诉编程人员如何使用在其芯片上进行编程:包括如何进行加减乘除计算,如何从内存中读取数据等指令操作。底层开发人员会根据不同指令集,适配不同的CPU处理器。计算机能执行的指令,又被成为机器语言机器码

前面所展示的二进制文件是一个可执行文件。什么是可执行文件呢?可执行文件就是二进制机器语言的集合,可以被机器执行,得到我们想要的结果。我们在Windows上常会遇到的.exe文件,就是可执行文件,exe其实是executable的缩写,从手机应用商店下载的APP也是可执行文件的一种变体。

C语言从源代码到可执行文件

很多朋友觉得C/C++编程调试难,没有比较就没有伤害,看到前文所提到的一个简单加法的程序竟然需要这么多看不懂的01代码,是不是觉得C语言简直是天才般的发明。是的,C语言的发明者当时考虑的就是不同芯片厂商有不同的指令集,相互之间难以兼容,于是想在那些晦涩难懂的底层语言上,建立一个更为通用的编程范式,这样编程人员不用浪费时间精力去识记大量的01二进制指令。那C语言代码是如何转化为可被机器执行的二进制文件呢?编译器和操作系统是两个非常关键的技术。

下面继续以加法计算plus.c源代码为例,展示编译器和操作系统计算机将C语言转化为机器可执行文件。

#include <stdio.h>

int main()
{
    int a = 1, b = 2, c = 0;
    c = a + b;
    printf("a = %d b = %d c = %d \n", a, b ,c);
    return 0;
}

Linux和Mac OS用户可以使用gcc -o plus plus.c这个命令来将plus.c的源代码编译成名为plus的可执行文件,plus会生成在当前的文件夹下。

执行这个二进制文件,结果将被打印到屏幕上:

$ ./plus
a = 1 b = 2 c = 3

gcc是一款开源的编译器,是GNU Compiler Collection中的一员,它可以将C语言代码编译成可执行文件。GNU Compiler Collection还有C++编译器g++、Fortran编译器gfortran,并且支持包括x86-64和ARM在内的不同指令集。

源代码编译执行过程

C语言从源代码到执行,要使用编译器来编译(compile)、汇编(assembly)并连接(link)所依赖的库,形成机器可执行文件。执行这个二进制文件时,操作系统会为程序分配内存和CPU资源。“编译”和“汇编”,相当于将C语言翻译成底层语言。另外,代码中使用了库函数printf,当我们使用别人写好的函数时,需要将这些前人写好的库函数连接到我们的可执行文件中,否则有调用函数失败的错误。我们将这种需要编译的语言称为编译型语言。编译型语言有C/C++、Fortran等。

操作系统和编译器是紧密相连的,不同操作系统所提供的编译环境不同。Linux和GCC编译器密不可分,Windows有自家研发的MSVC(Microsoft Visual C++)。不同操作系统在管理网络、读写硬盘、图形化等具体的实现方式不同,库函数连接方式不同…可执行文件一般需要调用这些操作系统接口,所以最终连接生成的可执行文件会截然不同。了解了编译知识,就不难明白为什么很多软件提供商对同一个软件会提供Windows、Mac OS、Linux、iOS、Android等多个版本的下载。因为不同平台的硬件、编译器和操作系统存在着巨大差异,可执行文件完全不同。所以,也就不难理解Windows软件为什么不可能在Mac OS上运行。

实际构建一个大型项目时,编译要考虑的问题会更多。比如我自己编写了多个文件,文件1会被文件2调用,所以要先编译文件1,后编译文件2,否则会因为顺序颠倒而报错;还比如编译型语言对所以依赖的库函数非常挑剔,如果版本过低,有可能出现编译错误。类似的问题会很多,因此编译型语言在编程和调试时更麻烦,实际操作中一般会使用构建工具链(toolchain),根据一定的顺序,从前到后串起来地去编译。

解释性语言:Java、Python、R…

既然可以将01组成的机器语言抽象成容易编写的C语言,那为什么不能继续再用类似的办法,再做一次包装呢?IT圈的一句名言就是:计算机科学任何领域的问题都可以通过增加一个中间层来解决。一些大牛忍受不了C语言这样编写和调试太慢,系统平台之间无法共享移植的问题,于是开始自立门户,创建了新的编程语言,最有名的要数Java和Python,这类语言不需要每次都编译,因此被称为解释型语言。matlab、R、JavaScript也是解释语言。

解释型语言执行过程

解释型语言一般是使用C语言等偏底层的语言做一个虚拟机或者解释器,编程人员需要先在自己的计算机上安装这个解释器,接下来就只用关心自己的源代码,其他的事情都交给解释器去做。如果把编译型语言的编译过程比作将源代码“翻译”成机器语言的话,那么解释型语言就是同声传译。编译型语言是一篇提前就“翻译”好的稿子,拿过来就能被读出来,这样肯定更快;解释型语言要等翻译边“听”边“翻译”,速度当然慢很多。

有了解释器,我们可以在任何安装了Python的机器上运行同样一份.py源代码文件。像Python这样的解释语言就像一个高级计算器,非常容易上手,有一些理工基础的朋友,半天时间就能学会。

其实,这就是一个妥协的过程,解释语言放弃了速度,取得了易用性和可移植性。

如果我还是关心速度呢?当然还是要回归底层,拒绝中间商赚差价嘛!

以Python为例,为了保证性能,大部分高性能科学计算库其实都是使用编译型语言编写的。比如numpy,用户安装numpy的包时,其实就是下载了C/C++和Fortran源代码,并在本地编译成了可执行的文件。Python用户自己可以使用Cython这样的工具,R语言可以使用Rcpp。我最近在使用Java来调用C++代码,速度有成倍提升。一些计算密集型的程序可以考虑用这种方法来进行优化。

另一种方案是JIT(Just-In-Time)技术。JIT把需要加速的代码编译成了机器语言,不再需要“同声传译”拖累自己了。我在Python上用numba库进行过JIT测试,同样的代码会有8倍以上的速度提升。

本专栏以后也将介绍如何对解释语言进行加速。

小结

北京后海 摄于2011年11月

计算机芯片的物理特性决定了它只能接受二进制指令。不同计算机芯片的指令集不同。高级编程语言需要转化成二进制机器语言才能被计算机所执行。编译型语言需要使用编译器经过编译和连接生成可执行文件,解释型语言需要使用解释器解释源代码。解释型语言更容易上手,但是运行速度更慢,必要时可使用C/C++重写或使用JIT技术加速。

本文分享自微信公众号 - 皮皮鲁的AI星球(ai-xingqiu)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-07-03

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java 里的 for (;;) 与 while (true),哪个更快?

    其次,for (;;) 在Java中的来源。个人看法是喜欢用这种写法的人,追根溯源是受到C语言里的写法的影响。这些人不一定是自己以前写C习惯了这样写,而可能是间...

    搜云库技术团队
  • 基于Django的电子商务网站开发(连载2)

    Django是基于Python语言的Web开发框架,所以要学习好Django,首先要有基本的Python开发技巧,以及需要了解HTTP协议的基本知识。本章介绍P...

    小老鼠
  • 如何更好的编写async函数

    Promise是使用async/await的基础,所以你一定要先了解Promise是做什么的 Promise是帮助解决回调地狱的一个好东西,能够让异步流程变得更...

    贾顺名
  • 深度学习的JavaScript基础:从callbacks到sync/await

    这篇文章就谈一谈JavaScript中的异步编程。文章参考了网上的一些资料,主要示例代码来自Async JavaScript: From Callbacks, ...

    云水木石
  • JavaScript异步编程:Generator与Async

    从Promise开始,JavaScript就在引入新功能,来帮助更简单的方法来处理异步编程,帮助我们远离回调地狱。 Promise是下边要讲的Generator...

    贾顺名
  • 教你如何用70 行 Go 代码打败 C!

    作为一名程序员,应当具有挑战精神,才能写出“完美”的代码。挑战历史悠久的C语言版wc命令一向是件很有趣的事。今天,我们就来看一下如何用70行的Go代码打败C语言...

    CDA数据分析师
  • async语法升级踩坑小记

    首先还是要谈谈改代码的理由,毕竟重构肯定是要有合理的理由的。 如果单纯想看升级相关事项可以直接选择跳过这部分。

    贾顺名
  • 【自考】数据结构中的线性表,期末不挂科指南,第2篇

    首先假定线性表的数据元素的类型为DataType ,这个DataType 可以是自定义的,也可以是默认的int,char等类型

    梦想橡皮擦
  • 简述 C语言 有和 C++ 的基本区别,你真的懂吗?(新手面试必学)

    c++:有命名空间:using namespace std(可以防止函数出现相同的情况)

    诸葛青云
  • ​70行Go代码打败C

    Chris Penner最近发表的这篇文章——用80行Haskell代码击败C(https://chrispenner.ca/posts/wc),在互联网上引起...

    AI科技大本营

扫码关注云+社区

领取腾讯云代金券