编程

概述

参考:

Programming(编程) 是编写程序的行为。

无论处于上层的软件多么的高级, 想要在 CPU 执行, 就必须被翻译成“机器码”, 翻译这个工作由编译器来执行. 编译器在这个过程中, 要经过“编译”,“汇编”,“链接”几个步骤, 最后生成“可执行文件”。可执行文件中保存的是二进制机器码, 这串机器码可以直接被 CPU 读取和执行。

编程学习资料

参考:

菜鸟教程

参考:

菜鸟教程提供了基础编程技术教程。

菜鸟教程的 Slogan 为:学的不仅是技术,更是梦想!

记住:再牛逼的梦想也抵不住傻逼似的坚持!

本站域名为 runoob.com, runoob 为 Running Noob 的缩写,意为:奔跑吧!菜鸟。

本站包括了 HTML、CSS、Javascript、PHP、C、Python 等各种基础编程教程。

同时本站中也提供了大量的在线实例,通过实例,您可以更好地学习如何建站。

本站致力于推广各种编程语言技术,所有资源是完全免费的,并且会根据当前互联网的变化实时更新本站内容。

同时本站内容如果有不足的地方,也欢迎广大编程爱好者在本站留言提供意见。

W3school

参考:

开发者资源网站(各种语言的教程、各种参考手册等等)

MDN

参考:

Mozilla Developer Network(Mozilla 开发者网络,简称 MDN) 官网,这是一个汇集了众多 Mozilla 基金会产品和网络技术开发文档的网站。

该网站都是前端所需要的东西

代码类型

参考:

所有的 Code(代码),都可以看作是 Instructions(指令)集合

  • C、Go 等高级语言编写的代码是源码,源码通过编译器,将源码转换为汇编码
  • 各种类型的汇编语言编写的代码是汇编码,汇编码通过汇编器,将汇编码转换为机器码

Source Code(源码)

在计算机编程中,Soure Code(源码) 是使用人类可读的编程语言编写的任何代码指令的集合。程序的源代码是专门为方便计算机程序员的工作而设计的,他们主要通过编写源代码来指定计算机要执行的操作。源代码通常由汇编器或编译器转换为可由计算机执行的二进制机器代码。然后可能会存储机器代码以供稍后执行。或者,源代码可以被解释并因此立即执行。

Machine Code(机器码)

在计算机编程中,Machine Code(机器码) 是任何符合 ISA(指令集架构) 的 Machine Instruction(机器指令)集合,用于控制对应 ISA 标准下的 CPU。其中的每条指令都会使 CPU 执行一项具体的任务。例如对 CPU 寄存器中的一个或多个数据单元进行 加载、存储、调准、算数逻辑单元 等操作。

Machhine Code(机器码) 也可以称为 Machine Instruction(机器指令)

比如,MIPS 架构为每条机器指令始终是 32 bit 长的机器码提供了一个特定的示例:

   6      5     5     5     5      6 bits
[  op  |  rs |  rt |  rd |shamt| funct]  R-type
[  op  |  rs |  rt | address/immediate]  I-type
[  op  |        target address        ]  J-type

MIPS 架构中将所有机器指令分为 3 类:

  • R-type(Register 寄存器) # 一般用来执行算术逻辑操作,里面有读取和写入数据寄存器的地址,如果是逻辑位移操作,还有位移量,最后的 funct 是功能码,用以补充操作码
  • I-type(Immediate 立即) # 通常用来执行数据传输、条件分支、立即数操作。
  • J-type(Jump 跳转) # 通常用来执行无条件跳转操作。每条指令后面通常都会接一条跳转指令以便 CPU 可以跳转到下一个位置执行后面的指令。

每种类型的机器指令最高由 32 bit 表示,不同类型的指令中每个 bit 所表示的含义是不一样的,通常前 6 bit 都是 op,用以表示这条指令具体需要执行的行为是什么。后面的 bits 则根据指令的不同而有所区别

  • op # Operation Code(操作码,简称 opcode) 也称为 Instruction machine code(指令机器码)、Instruction code(指令码)、Instruction syllable()、Instruction parcel、opstring。op 代表这条指令具体是一条什么样的指令。
    • op 码 与 实际行为 的对应关系,需要参考各个 ISA 规范
  • rs、rt、rd # 寄存器 XX
    • R 指令中 rd 是存放结果的目的寄存器
  • immediateaddress # 需要操作的“数”。
    • 可以是一个“具体的可以直接操作的数”或“存放数的地址”
  • target address # 目标地址

若一个 CPU 是 32 位 或 64 位,那寄存器中可以存储的 bit 数即为 32 bit 或 64 bit

比如我们可以通过 I-type 指令将数据存储到指定的寄存器中,然后通过 R-type 指令计算指定寄存器中的数据,并将结果放到另一个寄存器中,最后通过 J-type 指令跳转到下一个位置继续执行后续的指令。

简单示例:

将寄存器 1 和 2 相加并将结果放入寄存器 6 的编码如下:

[  op  |  rs |  rt |  rd |shamt| funct]
    0     1     2     6     0     32     十进制表示
 000000 00001 00010 00110 00000 100000   二进制表示
                0X????                   十六进制表示。怎么转换还没找到资料

将一个值加载到寄存器 8 中,该值取自寄存器 3 中列出的位置之后的存储单元 68 个单元:

[  op  |  rs |  rt | address/immediate]
   35     3     8           68           十进制表示
 100011 00011 01000 00000 00001 000100   二进制表示
                 0X????                  十六进制表示。怎么转换还没找到资料

跳转到地址 1024:

[  op  |        target address        ]
    2                 1024               十进制表示
 000010 00000 00000 00000 10000 000000   二进制表示
                 0X????                  十六进制表示。怎么转换还没找到资料

总结

机器码的结构其实和各种协议的封装结构非常类似,都是通过某些规范,将 bits 划分为几块,每块 bits 数表示的含义是不一样的

而决定机器码结构的,就是 ISA(指令集架构) 了,根据 ISA 生产的 CPU 在处理机器码时,会根据自身的 ISA 来解析这些机器码,隔几 bit 识别一次,然后执行识别到的结果,若是 CPU 在识别机器码时发现是在自己的 ISA 规范中,那么这条机器码将会被转成微码并在 CPU 内流动,若机器码错了,那么是没法通过 CPU 的译码阶段,控制点路将会报错。这时如果是 windows 系统往往就会蓝屏,因为 CPU 无法识别机器码中的指令,不知道自己应该执行什么。

机器码与源码最大的区别在于

  • 源码可以用多种高级语言编写;而每条机器码是与每行汇编码是一一对应的
  • 源码是人类可读的;机器码人类读不懂
  • 源码通常都是文本;机器码是 0 和 1 数字的集合(当然,二进制也可以通过某些规则,编码为 10 进制、16 进制等,用于传播与存储)

最后说一点:

  • 源码是与人交互的,需要符合人的规矩,也就是各种高级编程语言的格式
  • 机器码是与机器交互的,需要符合机器的规矩,也就是各种指令集架构

所以,就算是好多好多 0 和 1 的数字,也需要符合某些规范,才可以被 CPU 识别。当 CPU 识别时,假如现在我规定,每隔 10 bit 识别一次,然后发现 10 个 01 组成的数字,在自己的规范中没有任何描述,那么这 CPU 也就执行不下去了。这也是一个程序无法在多种架构的 CPU 上执行的原因。因为当一个程序经过汇编之后,01 的排列是以一种架构实现的,这种排列方式在其他架构上是识别不出来的~~~~

机器码与汇编码

由于机器码对人类来说非常不可读,所以早期人们创造了汇编语言,汇编语言的作用就是使用人类可读的汇编码与机器码建立一一对应的关系,这样,人们在编写程序时,就不用一直编写 01 了,而是使用单词来描述一条指令。

以上 C 语言编译成汇编语言,再由汇编语言翻译成机器码,也就是 0 和 1 的机器语言。一条条的 16 进制数字,就是机器能读懂的计算机指令

linux 系统上可以使用 gcc 和 objdump,把汇编码和机器码都打印出来。汇编代码和机器码是一一对应的,一条汇编对应一条机器码。

可以说,汇编码就是给“给人看的机器码”

通常,同一个 ISA 规范下,每条机器码,通常都对应一行汇编代码~~

Bytecode(字节码)

Bytecode(字节码) 也称为 p-code(p 码),是一种特殊的可以被执行的机器码,只不过被 “虚拟机”(i.e. p-code machine,是一种解释器,portable code machine)执行。之所以称之为字节码,是指这里面的操作码(opcode)是—个字节长。一般机器指令由操作码和操作数组成,字节码(虚拟的机器码)也是由操作码(opcode)和操作数(op)组成。对于字节码,它是按照一套虚机指令集格式来组织。

这里提到的虚拟机,对于不用使用场景来说有不同的代指,比如 Java 中执行字节码的是 JVM;在内核中执行 BPF 字节码的是 BPF 虚拟机;等等。

指令 & 指令集 & 指令集架构

详见:指令集架构

模块 & 包 & 库

Module(模块)、Package(包)、Library(库) 常用来描述一个项目的组成。模块与包之间的包含关系在不同的编程语言中有不同的理解

  • 比如 Go 中一个目录就是一个包,多个包的项目是模块
  • 而 Python 则是一个文件就是模块,多个文件就是一个包。

但是不管如何规定,总得来说,都是编程语言们为了复用代码而抽象出来的概念。不管是包还是模块,这些东西组合在一起总要有一个地方保存,这个地方就称为 Library(库),具有图书馆之意。

标准库一般用来表示编程语言自身的一些基本功能的集合。比如输入输出控制、系统调用、时间、路径、数学、等等功能。

第三方库一般用来表示由编程语言编写的扩展功能的集合。这些功能往往比官方提供的基础功能强大,比如高级的数学计算能力等等。

程序是怎么一步步变成机器指令的?

参考:

大家好,我是小风哥,今天简单聊聊程序是怎么一步步变成机器指令的。

左边是我们写的代码,右边是CPU执行的机器指令:

想让CPU执行代码只需要简单的点击一下这个按钮:

可是你知道这个按钮的背后经历了哪些复杂的操作,你有没有想过代码是怎么一步步变成机器指令的?

程序员编写的程序实际上就是一个字符串,必须得有个什么东西把字符串转变从机器指令,它的输入是字符串,输出是01二进制机器指令,这就是编译器。

编译器本身就是一个程序,把人类认识的程序转为CPU可以执行的机器指令。

假设有这样一段代码:

这实际上就是一个字符串,编译器要做的第一件事就是遍历字符串并把有意义的字符组合提取出来,忽略掉空格换行等字符。

这里每一个字符组合实际上都有类型,比如int 和main都是关键字,0和5都是数字等,因此还需要标注好类型,这一步就是所谓的提取token。

提取出token之后还需要知道这些token组合在一起的含义是什么。

接下来遍历所有token进行解析。

按照什么解析呢?答案是按照语法。

假设编译器接下来发现token是if,那么很显然,接下来会判定这是一个if语句,那么接下来就按照if语句的语法来解析。

编译器在按照语法解析时会生成一颗树,首先匹配的是if本身:

接下来是左括号:

括号之后是布尔表达式:

布尔表达式之后是右括号以及大的左括号。

接着是if内部的语句:

注意看,根据语法解析token后生成的这棵树就叫做抽象语法树:AST。

接下来,编译器遍历这颗抽象语法树并生成指令:

当然真正的编译器可能并不会在这里直接生成机器指令。

我们知道CPU只能执行一种类型的机器指令,x86处理器只能执行x86机器指令,arm处理器只能执行arm机器指令:

如果你发明了一种语言,为了适配不同的处理器自己需要针对每一种处理器编写相应的后端部分。

要是有一种工具能帮我们完成针对不同处理器的适配工作就好了,这就是LLVM,我们可以只生成针对LLVM的中间代码,由LLVM处理剩下的部分。

这就是生成中间代码的好处。

值得注意的是,编译器在生成指令时会进行优化,这个示例中变量a实际上没什么用处,编译器会注意到这一点并把针对变量a的赋值指令去掉。

得到汇编指令后编译器会最终将其转为CPU可以认知的二进制机器指令,每个源文件被编译后都会生成一个目标文件,目标文件中就是转换后的二进制机器指令。

最后,链接器会把目标文件打包成最终的可执行程序,