电脑怎样执行编程语言的?

2023-05-16


链接:https://www.zhihu.com/question/29227521/answer/154819061
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这个问题真的是很大,让我们自顶向下的解释。


在顶层,程序员编写出来的都是源代码。源代码可以使用各种高级语言写成,例如 c/c++ c# java python 等等;也可以使用对应平台的低级语言写成,例如汇编。想必你已经了解其中的过程了。

到这一步为止,距离最终机器可以执行的指令还有一大步要走。

首先要面临的一个问题是:源代码都是以人类语言写成的。即便是能够和机器指令一对一翻译的汇编代码,依然是人类语言。计算机无法理解其中的含义,所以不可能执行。

所以我们需要将人类语言翻译为计算机语言。计算机能听懂的语言,就叫做机器语言,简称机器码。


在这里说几句题外话。

在计算机历史的上古时代,大概是上个世纪50年代之前。那时编译理论和形式语言还没有得到发展。几乎所有的程序都是直接由机器码写成的。比如由工程师直接将二进制机器码和数值编写在打孔卡上,通过读卡机读入计算机存储器,然后执行。

而打孔卡长这个样子:

<img src="https://pic1.zhimg.com/50/v2-c8536a9e501f9e21ef1dd6d9ef0a6096_hd.jpg" data-rawwidth="2232" data-rawheight="1004" class="origin_image zh-lightbox-thumb" width="2232" data-original="https://pic1.zhimg.com/v2-c8536a9e501f9e21ef1dd6d9ef0a6096_r.jpg">

(来自 wiki,80列标准 IBM 打孔卡,你能读出上面是什么意思吗?)

计算机的基本架构虽然经过了将近百年的发展,但是核心的模型倒是一直很稳定,都是存储程序模型。

首先将程序指令从外存(打孔卡,磁带,硬盘,软盘,光盘,闪存卡,网络等)读入内存,然后让处理器从内存按顺序取指执行,结果写回内存中。

在那个年代,人们对程序运行原理的理解是不存在什么障碍的。工程师怎么写,计算机就严格的按照指令执行。每一条指令对应一个步骤。最后的到结果。

在这种条件下,程序开发绝对是顶尖的职业,首先能够理解目标机的架构就需要相当的功夫了。其次还要按照机器的方式思考,写出正确无误的指令序列。

这样的开发过程无疑限制了计算机行业的发展。

同时,即便是擅长于按照机器方式思考的工程师,也认为机器指令太难记了。如你所见,在打孔卡上准确无误的写上指令真是头疼的要死。所以工程师们开发了一套助记符,用来指示对应的机器码,这样以来,程序的编写和 debug 就方便多了。到上世纪40年代末期,就已经有一批成熟的助记符体系了。

<img src="https://pic1.zhimg.com/50/v2-167f82b471c3e2f12622f731a9e5cbd9_hd.jpg" data-rawwidth="1330" data-rawheight="940" class="origin_image zh-lightbox-thumb" width="1330" data-original="https://pic1.zhimg.com/v2-167f82b471c3e2f12622f731a9e5cbd9_r.jpg">

(ARM v7 汇编指令卡中的某一页)

关于助记符的话题,暂且搁置。


回到正题。为了将人类语言翻译成机器变成机器能够理解的语言,还需要进行翻译。就好像你不懂英语,英语可以翻译成汉语,这样你就能明白其中的含义。对于计算机来说,这个过程是一样的。不过计算机对于翻译有更高的要求。人类之间互相翻译语言,有一些微小的出入并不影响理解,计算机为了能够准确的得到结果,要求这个翻译的过程,必须保证“将一种语言翻译成涵义相同的等价的另一种语言”。

在早期,程序的规模还比较小,翻译的过程可以人工的进行。利用查表的方式,最终是可以得到等价的机器码序列。随着计算机科学的发展,程序规模膨胀的越来越快,人工翻译变的没有可行性。此时就有人提出,编写一套软件来进行这个翻译的过程。

一开始人们只用汇编语言进行程序开发。所以只需要将汇编语言翻译为机器语言就可以了。这是相当直截了当的过程,因为汇编语言的助记符和机器指令是一一对应的关系。所以只需要完成一个能够自动查表并转换的程序即可。很快,这样的程序就被发明了出来。我们称之为“汇编器”。

伴随着汇编器的发展,工程师又开始想要偷懒。他们认为,既然汇编器可以将汇编指令翻译成等价的机器码,那么在翻译之前一定也可以做一些预先处理的工作,将一个助记符转换为多个助记符组成的序列。这样以来,开发人员就可以使用较少的代码,写出较多的内容。同时将常用的一些程序结构编写成对应的助记符,在需要时就使用这个助记符,还可以帮助开发人员减少程序出错的可能。简直太好了。于是,人们又在汇编器中引入了宏指令。

所谓“宏(macro)”就是一套预先定义好的指令序列。每当汇编进行的时候,先预处理一次将宏等价的展开,然后再进行翻译。如此,源程序变的更加容易理解了。


宏的引入,催生了程序结构化表达。在今天的汇编语言当中,我们也可以像使用高级语言的 if else for while 语句一样,使用等价的结构语句。只不过,汇编中的结构语句都是宏实现的。


结构化表达给了一些计算机科学人员启发。能不能更进一步,使用完全结构化,脱离某个对应机器平台的形式化语言来描述一个源程序?于是,就有了高级语言及其编译器。

开发人员利用高级语言编写程序,然后利用对应的编译器生成中间代码,最后再将中间代码变成机器码。中间代码可以是等价的汇编代码,也可以是其它类型的代码例如 JVM 的字节码。最终处理中间代码的程序可以是一个对应平台的汇编器,也可以是一个解释器。在这里姑且隐去这些细节,将编译的最终产物都视为一系列可以被执行的二进制机器码。关于编译器的更多内容,在网上可以找到很多详细的资料。在这个话题下,编译器不是核心问题,我就不再深入讨论了。


至此,就得到了一个可以被执行的程序了。这个文件的内容是一系列二进制指令和数据组成的序列。它能被装入机器的内存,并且可以被处理器解码执行。


但是,为什么是二进制


说回来,计算机其实是长期使用的一个简称。严格的讲应该叫做“电子计算机”。但是计算机的形态并不限于电子式计算机。算盘,计算尺,对数计算表都可以算作广义上的计算机,同时在电子式计算机出现之前,它的还有一个机械式计算机的表亲。

<img src="https://pic3.zhimg.com/50/v2-39e7480eed57ce4010d52f631b34e451_hd.jpg" data-rawwidth="1704" data-rawheight="2272" class="origin_image zh-lightbox-thumb" width="1704" data-original="https://pic3.zhimg.com/v2-39e7480eed57ce4010d52f631b34e451_r.jpg">

(来自 Wiki 。 查尔斯·巴贝奇 的分析机。蒸汽动力驱动,采用十进制,其内存能够存储1000个50位的十进制数,相当于20.7 KB 的 SRAM 或 DDRAM。采用打孔纸带读入程序,具有类似汇编语言的助记符编程系统,是图灵完备的。很蒸汽朋克,嗯?)


可是我们并不认为算盘以及计算尺和现代计算机是同一个东西。最核心的区别在于,现代计算系统是可编程的。按照这个定义,上面的分析机也是现代电子是计算机的鼻祖。它身上的核心模型一直继承至今。

在分析机上,已经实现了 “ 存储程序计算机 ”。

这也就是现代计算系统的基本概念:

  1. 以运算单元为中心
  2. 采用存储程序原理
  3. 存储器是按地址访问的,线性的空间
  4. 控制流由程序的指令流产生
  5. 指令由操作码和操作数组成

这一概念所描述的计算模型具有以下的过程:将完整的程序装入存储器后,运算单元依照地址按顺序的从存储器中取出指令和数据且执行。指令序列就像流水一样“流”入运算单元,当指令流尽,就意味着程序结束了。

<img src="https://pic1.zhimg.com/50/v2-4dff85953dd98440a398a57f3a7e5c8e_hd.jpg" data-rawwidth="543" data-rawheight="223" class="origin_image zh-lightbox-thumb" width="543" data-original="https://pic1.zhimg.com/v2-4dff85953dd98440a398a57f3a7e5c8e_r.jpg">

对于计算机,自然是希望运算的速度越快越好。所以机械式运算很快就淘汰了。取而代之的就是电子式计算机。


电子式计算机的硬件基础,就是数字电路。因为二进制可以很自然的表示开和关的两种状态,高和低的两种状态,通和断的两种状态,等等。所以很快就取得了主导低位,其它进制的数字电子器件沦为小众。

理论上,二进制和十进制表示的数的范围是一样多的。因为实数集是一个连续同,不同进制实质上是对数集的不同分割。


基于二进制数字电子器件制造的电子式计算机自然就需要二进制的输入输出。


到了这个层次,我们基本上解释了高级语言源程序是如何成为计算机可以识别的二进制指令序列的。接下来的问题是,计算机如何识别并执行二进制指令呢?


通用处理器被称为“通用”,就是因为它不限定于特定用途。路边上买一个计算器。只能计算四则运算,而计算机还能进行字处理,可以玩游戏看电影。都有赖于通用处理器提供的运算能力。

为了实现通用的目标,处理器在设计之初就不能对未来可能进行的运算进行限制。但是未来的可能性是无穷的。处理器不可能穷尽所有可能。

所以,处理器提供了一套它能够支持的运算操作的集合,称为“指令集”。指令集就限定了该处理器能够进行的所有运算。而且这些运算通常都是有关于数字的运算。如果我们想解决一个任意问题,那么首先要把这个问题转换为一个数字问题,再把数字问题的解答过程,用指令集当中的指令求解。

将其它问题转换为数学问题的一种方法就是编码。比如我们常见的 ASCII 码表,就是把英语字符数字字符以及电报传输过程中的控制字编码成对应的数字。例如字符 a 就等于数字97。


处理器的指令集同样是经过编码的。所以我们才能用二进制数字流来表示指令。

举个例子。在一个典型的 Intel IA-32 处理器上所支持的 x86 指令集。假设我们想将一个字节的数据从内存移动到 al 寄存器,不妨就让这个数据在内存中 0x20 (十六进制表示的32)号字节的位置好了。那么,我们要写出汇编代码:


mov al, 30h

将这一行代码送入汇编器,得到对应的机器码为:


0xB0 0x20

二进制的表示为:


1011 0000  0010 0000

其中 0xB0 就是我们的指令,也就是执行第 176 号指令。这条指令的意思是:从内存中指定的位置搬移数据一个字节宽度的数据到 al 寄存器。地址由紧跟在本指令后的数给出,在这里就是 0x20。

指令集中的每一个指令都可以这样编码。每一条指令都定义了一系列的操作。

如此,只要按照顺序的从存储器读入指令代号和数据,就可以让程序执行下去。

你又要说了,那如果我有循环,有条件判断怎么办?

简单。处理器为了能顺序的取指并执行,需要知道当前指令的下一条指令在哪里。为什么不是这一条指令在哪了?因为这一条指令已经取回来了,所以它在哪里就不重要了。为了记录当前指令的下一条指令的位置,处理器内部有一个存放了这个地址的电子装置,实现上它是一系列门电路组成的锁存器,叫做 IP 寄存器(也有叫做 PC 的,这里统称为 IP)。IP 的值可以在运行时被修改。那么只要提供了能够修改 IP 值的指令,就能改变程序的执行流程。可以返回到之前的某个位置,也可以一次前进到之后的某个位置。这个过程叫做“跳转”。

所谓循环和判断,本质上都是判断并跳转。

用一个程序来做一个直观的说明。这个程序很简单。求出一个数组中所有数的和,然后返回这个值,如果这个值是0,则返回一个 -1。

和它等价的 C 代码如下,这里我们将结果返回运行时:


int main(void) {
    int numbers[5] = {1, 2, 3, 4, 5};
    int result = 0;

    for (size_t i = 0; i != 5; ++i) result += numbers[i];

    return (result == 0 ? -1 : result);
}

编译器产生的汇编文件长什么样子呢?长这样的:


CONST	SEGMENT
constNumbers: 0x01, 0x02, 0x03, 0x04, 0x05
CONST	ENDS

TEXT	SEGMENT
numbers SIZE 20 BYTE

main	PROC

	sub	esp, 20	
	movaps	xmm0, XMMWORD PTR constNumbers

	xor	eax, eax
	push	5
	pop	edx
	movups	XMMWORD PTR numbers[ebp], xmm0
	mov	DWORD PTR numbers[ebp+16], edx

	mov	ecx, eax
Loop:
	add	eax, DWORD PTR numbers[ebp+ecx*4]
	inc	ecx
	cmp	ecx, edx
	jne	SHORT Loop

	or	ecx, -1
	test	eax, eax
	cmove	eax, ecx

_main	ENDP
_TEXT	ENDS
END

为了便于解释,这里隐去了很多细节,并且使用了很多伪代码。上面的汇编程序是不经修改是无法通过编译的。

等价的二进制文件又是什么样子呢?为了方便阅读,我稍稍整理了一下,并且加上了对应的汇编代码,它长这个样子:

<img src="https://pic4.zhimg.com/50/v2-8b6115a3b19af8d6b459aeb45b0223b9_hd.jpg" data-rawwidth="658" data-rawheight="307" class="origin_image zh-lightbox-thumb" width="658" data-original="https://pic4.zhimg.com/v2-8b6115a3b19af8d6b459aeb45b0223b9_r.jpg">

(第8行操作数应当分为两列,这里有一个小错误。)

同样的,还是省去了很多细节。绿色的部分就是机器码。

我完全理解使用助记符和高级语言的重要性。否则谁能通过机器码一眼看出一段程序的含义呢?


当程序装入内存以后,IP将被(另外的某个程序,可能来自操作系统,或者其它软件)设置为 1,意思是:下一条要读取的指令在 1 的位置。然后处理器就开始读入指令。

为什么处理器会读入指令呢?它是收到某个信号才会读指令吗?简单的讲,处理器从上电到掉电的整个过程当中只做三件事情,那就是:


  1. 从内存读取一条指令和指令携带的操作数,同时 IP + 1
  2. 解码并执行指令
  3. 回到 1

所以不需要什么信号。在上一条指令将 IP 的值修改为 1 之后,处理器就已经完成跳转,找到程序入口了。

处理器取指,读入第一条指令 0xce83。这里要插入一点,Intel 的处理器采用的是小端数据格式,就是说一个数的高位放在地址较高的地方,低位放在地址较低的位置。所以要倒过来读,在这里就不详细解释了,略过。

处理器将这条啊指令送入解码器,解码的结果告诉处理器,应当执行“将 esp 寄存器中的值减去一个指定数,该数由紧随指令的连续四个字节指定”的操作。然后处理器通过数据总线连续读入四个字节,得出操作数应该是 0x14(十六进制的20)。接着就执行了这个操作,IP + 1,继续取出下一条指令。

这个过程是很好理解的。总之就是这样的循环。直到断电。


再注意一下行号 11 和 12 标识的代码。11 行将执行比较 ecx 寄存器中的值和 edx 寄存器内的值。根据不同的结果,12 行指令将有不同的行为:


  • 两个值相同的时候,12 行指令什么也不做,IP + 1。
  • 两个值不同的时候,12 行指令会将 Loop 标号的地址写入 IP。 IP = 9。

程序走着走着就走回去了。这就是比较与跳转。简单吧。

而 10 行的代码将会使 ecx 寄存器内的值增长,每次经过 10 行都 +1,随着循环的进行,程序流不断的跳转到 9 行,然后经过 10 行。在某一次经过后,ecx 等增长正好令 ecx = edx 成立。这时候 12 行将什么也不做,IP 指向 13,程序又继续进行下去了。


接下来,进入处理器的层次来理解它如何工作的。在这里我们要讨论四个问题:

  1. 指令是如何表示的?
  2. 数据是如何取回的?
  3. 指令是如何解码的?
  4. 指令是如何执行的?

程序运行的过程,上面已经提到过了。程序是完整的装入内存中的。运算器能够直接操作的只有存储器中的数据。他们之间的硬件连接如图所示:

<img src="https://pic3.zhimg.com/50/v2-3b06f9699950184c4b4c210d145eb13c_hd.jpg" data-rawwidth="648" data-rawheight="424" class="origin_image zh-lightbox-thumb" width="648" data-original="https://pic3.zhimg.com/v2-3b06f9699950184c4b4c210d145eb13c_r.jpg">

sorry,搞错了,是这个:

<img src="https://pic2.zhimg.com/50/v2-1ad9ef2a8a07b07281896f4f040c9457_hd.jpg" data-rawwidth="800" data-rawheight="668" class="origin_image zh-lightbox-thumb" width="800" data-original="https://pic2.zhimg.com/v2-1ad9ef2a8a07b07281896f4f040c9457_r.jpg">

图上黄色的一根粗线其实以一排并列的导线,在这里是 8 根导线并列在一起。只是看起来画在了一起,其实是互相分开的。

使用 8051 及其外部扩展存储器接口电路来说明问题主要是为了简便。在不失准确性的前提下,我依然隐去一些细节,方便理解。


访问存储器的过程主要关注两个问题:

  1. 送出地址
  2. 取回数据

考虑一般的访问过程,当运算器执行如下操作时:


mov al, 0xD0D0

将会发生什么呢?

首先 mov 指令指定了数据搬移的操作,第二个操作数是一个立即数参数,直接给出了地址。现在就要到存储器当中去找这个地方了。

处理器不能直接操作存储器的具体单元,但是它可以请存储器将对应单元中的数据准备好,然后取回来。你肯定有过取快递的经历,菜鸟驿站去过吧,门市点不会让你亲自进仓库去找快递的,但是你可以告诉快递小哥你的单号,然后他进去帮你找到,最后把包裹交给你。内存和这一个意思。

处理器首先将地址放到地址总线上,地址总线就是图上 D0-D7 和 A8-A15 这15 根导线组成的。

处理器将自己的端口设置成对应的值,就把地址放到了总线上。0xD0D0对应的端口状态应该是:

<img src="https://pic2.zhimg.com/50/v2-d914aedb579fae88ba0d43ca7d4fc43e_hd.jpg" data-rawwidth="1155" data-rawheight="41" class="origin_image zh-lightbox-thumb" width="1155" data-original="https://pic2.zhimg.com/v2-d914aedb579fae88ba0d43ca7d4fc43e_r.jpg">

(图有点小)

然后,处理器告诉存储器,我准备好取数据了,地址在总线上,请你准备数据。具体的方式就是拉低 \overline{OE} 端口的电压到地电位(一般就是 0V)。存储器得到这个消息后,就从总线上取得地址。然后解码这个地址,找到对应的数据,假设数据是0x11吧,然后把数据再放回总线 D7-D0上。

处理器在发出取指指令后会等待一段时间,然后就从总线上取回数据。取回的数据就当做存储器的回应。至于这个等待的时间具体多长,这是两个设备间相互约定好的。不需要关心。

最后,将总线上取得的数据 0x11 放入 al 中,指令完成。


可能有的读者就很迷惑了,为什么放到总线上就能传递数据呢。

真实的情况是,总线上传递的是电压的信号。这也是为什么使用二进制方便的原因。总线就是一组导线,在这一组导线上,一一对应的连接了处理器和存储器的端口。虽然电子在导体内的移动速度很慢,但是电场的传播速度却是光速。所以当总线一端的端口建立了电位之后,另一端的电位将立刻改变。此时信号就已经从一个器件传递到了另一个。器件之间信号的传递,依赖的就是端口上电压的改变。器件对总线数据的读取,就是读取端口上电压的高低。而二进制可以使这个问题变的很简单。只要端口上能够反应电压的高和低区别就足以传递信号了,一般的,高电位的区间在 3.3V - 5.0V 之间,而低电位在 0V - 2.2V 之间。考虑到总线都是板级的传输,距离很近,总线上电场传播所需要的时间可以忽略掉。那么一组总线传播数据的速度就取决于其两端端口上电位改变的速度。这可比读卡器读卡高了不知道哪去了。也比磁盘寻道和读取快的多。


在数字电路中,我们一般用 0 表示低电平,用 1 表示高电平。

上面提到过,mov 指令的编码是 0xB0。这个编码是什么意思呢?将其写作二进制会发现


0xB0 = 1011 0000

刚刚我们已经介绍过了。0和是表示的就是电压的高低。现在一切都清楚了。数据的编码其实就说说的端口上电压的高低状态。如果处理器的输入端口在读入指令时读入的端口情况是从 D7到D0为 高低高高 低低低低。那么就读入了 1011 0000。


那么我们已经知道如何取数据了。取指令也是一个方法。只不过取指令的过程是自动的,指令的地址总是 IP 的值。取回的指令总是送入指令解码器当中。

根据读入数据时处理器所处的不同阶段,将会给读入的数据一个不同的解释。读指令阶段就会把数据送入解码器。读数据阶段就会把数据送入另外的地方。


接下来,就需要进行指令解码。指令解码本身也是一个非常大的话题,其实单独拿出来也可以写出和本篇一样长的文章了。在这里只能概略的介绍一下。


处理器本身要完成某些特定的运算,在硬件上是需要某些特定结构的电路的支持的。比如你要完成一次加法,就需要一个带有加法器的电路。完成一次位移,就要有带移位寄存器的电路。简单的说,任何一条指令,都需要一组特定的电路来提供支持。但是人们通过长期的对数字电路的研究发现。几乎所有的运算,都可以通过有限的几种器件的不同组合来完成。这样的话,我们的通用运算器当中,可以包含一些要素器件,然后通过运行时改变它们的连接来实现不同的功能。这就是我们思考指令编码的方向。

其实在电子式计算机刚刚诞生的时候,就已经实现基本运算器的复用了。运算中心中包含了一组基础的运算器,它们的输入输出端口上同时连接了很多组不同的电路,每执行一条指令的时候,都选择其中特定的一组电路,使其生效,而让剩下的电路失效。这样在指令执行的过程中,这一组执行电路就可以独占整个运算器。当运算结束拿到结果后,电路再将运算器释放掉。就可以准备下一次的运算了。

在早期,还没有指令编码技术。要使用不同的指令,必须改变电路间的硬连接。也就是要把一组插头从这里拔下来插到另外的地方去。世界上第一台通用电子计算机 ENIAC 的操作方式就是如此。编程的方式是女工进机房去接插头。

<img src="https://pic1.zhimg.com/50/v2-42cc3730a33fa5303e52eaa50dfd91ce_hd.jpg" data-rawwidth="382" data-rawheight="214" class="content_image" width="382">

(假设我们有三条可编程指令流水线,那么如果我们想依次执行数据转移,异或,求和的操作,就需要连接 #1 的 move, #2 的 xor 和 #3 的 add)

而后出现了指令编码。送入的指令被解码器解码后,自动启动一组对应的电路。

<img src="https://pic3.zhimg.com/50/v2-af944f1f1713ade91eb16c40e468a422_hd.jpg" data-rawwidth="403" data-rawheight="180" class="content_image" width="403">

这样说也许很难让人明白“自动”的含义。所以我在这里实现一个简单的编码指令处理器。在这里我们只实现 3 条指令:

  • 指令0:将输入端的数据存入寄存器 a
  • 指令1:将输入端的数据存入寄存器 b
  • 指令2:取寄存器 a 寄存器 b 中的值求和,将结果放入寄存器 a

在这里我们只研究解码,不管其它的因素,这样就简化了问题。不多说,直接上图:

<img src="https://pic2.zhimg.com/50/v2-87814e38f146df96bee0b3d745ef38be_hd.jpg" data-rawwidth="1182" data-rawheight="937" class="origin_image zh-lightbox-thumb" width="1182" data-original="https://pic2.zhimg.com/v2-87814e38f146df96bee0b3d745ef38be_r.jpg">

最顶上的 instraction register 和 instraction decoder 的部分就是指令解器。首先将指令读入一个寄存器,然后解码。实际的运算器也是这样的流程。图中蓝色的就是数据总线,寄存器内的值分别是两个寄存器 Qx 端口上的值。

让我们启动他它,来算一下 0x10 + 0x0F (16 + 15)是多少。

<img src="https://pic3.zhimg.com/50/v2-f36b90c5cb14f23d542e43e9d40779cf_hd.jpg" data-rawwidth="1182" data-rawheight="936" class="origin_image zh-lightbox-thumb" width="1182" data-original="https://pic3.zhimg.com/v2-f36b90c5cb14f23d542e43e9d40779cf_r.jpg">

上电之后,我们注意到:

  • 寄存器 a 和寄存器 b (右下两个)都被初始化为 0xFF
  • 输入端口(左下角)上的值为 0x00
  • 指令寄存器(上方)当中当前的指令为 0x0F (15号),这是一条未定义的指令,所以没有任何效果。

首先我们要执行


mov a, 0x10

当指令读入后,在指令输入端将会是 0x00 的状态,译码输出端口上输出全 0,指示出目前要执行0 号指令。同时选中了 0 号指令的执行电路。

数据端的输入为 0001 0000。mov 命令的状态下,输入选择器选择输入端口的数据放到总线上。同时,寄存器 a 被激活,将总线上的值存入:

<img src="https://pic1.zhimg.com/50/v2-11a49ccbdf02d9f65d7bd431bea04fce_hd.jpg" data-rawwidth="1182" data-rawheight="936" class="origin_image zh-lightbox-thumb" width="1182" data-original="https://pic1.zhimg.com/v2-11a49ccbdf02d9f65d7bd431bea04fce_r.jpg">

(可以看到 0x10 已经被存入寄存器了)

第二条指令,我们将启动寄存器 b,然后存入数据。指令为:


mov b, 0x0f
<img src="https://pic1.zhimg.com/50/v2-f10de0d8cfc7dd98ee94d89db11c4332_hd.jpg" data-rawwidth="1182" data-rawheight="936" class="origin_image zh-lightbox-thumb" width="1182" data-original="https://pic1.zhimg.com/v2-f10de0d8cfc7dd98ee94d89db11c4332_r.jpg">

instraction decoder 的 1 号输出被选中,此时激活了 1 号指令的电路。输入端的 0x0F 被存入了寄存器 b。而寄存器 a 中的值保持不变。

第三条指令,2 号指令,求值。


add

没有给出操作时是因为操作是已经隐含的指明了,就是 a 和 b:

<img src="https://pic1.zhimg.com/50/v2-727b3d793df16781896a5d97d67b7742_hd.jpg" data-rawwidth="1182" data-rawheight="936" class="origin_image zh-lightbox-thumb" width="1182" data-original="https://pic1.zhimg.com/v2-727b3d793df16781896a5d97d67b7742_r.jpg">

译码器的 2 号输出选中了。全加器完成了运算(左侧是第四位,右侧是高四位),结果放上了总线,被锁存到了加法器的输出缓冲器当中。

同一时间,双输入选择器也被激活。它截断了输入端口的连接而选择加法器输出缓冲值作为输入,将其放上了总线。

寄存器 a 从总线取得数据,存入。完成了指令。


看看,结果是 0x1F,恰好就是我们预期的 31。


实际中的处理器的处理过程比这个复杂得多。这里为了方便理解,做了很多简化。但是概念都是相同的。处理器自动的从内存中读入指令和数据,然后解码,启动对应的电路,最后拿到结果。如此往复。


到此为止,已经几乎完全说明了计算机的运算原理,以及高级语言和机器语言的关系。但是我们依然可以更进一部,探究一下数字电路的构成。编码器是怎么运行的?寄存器是怎么锁存数据的?


上面一直在说解码器,那么解码器到底是什么?

处理器内部的指令解码器可能非常复杂,也许是一个器件,也有可能是一组器件,或者是可编程的硬件电路(对的,硬件电路也是可以编程的,例如 FPGA)。

而这里,我在上面的例子中使用的解码器:74LS42 4 Lines to 10 Lines BCD to Decimal Decoder (4线10线BCD译码器)的内部结构是这样的:

<img src="https://pic3.zhimg.com/50/v2-6698baf03b1f6bf7d5e027d4090ca48f_hd.jpg" data-rawwidth="428" data-rawheight="414" class="origin_image zh-lightbox-thumb" width="428" data-original="https://pic3.zhimg.com/v2-6698baf03b1f6bf7d5e027d4090ca48f_r.jpg">

可以看到,BCD 输入端(左边)输入后首先连接了非门(NOT),然后进入一个选择矩阵,最后通过三入与非门(NAND)输出。


与门(AND)、或门(OR)、非门(NOT)是数字电路中,最基础的三种逻辑门电路。它们的组后构建了大量的实用器件。

关于三种逻辑门,它们的特性可以使用真值表来表示:

<img src="https://pic2.zhimg.com/50/v2-9620ecea93ad929c25f8e95adb4a6138_hd.jpg" data-rawwidth="291" data-rawheight="231" class="content_image" width="291">

(1 代表真,0 代表假)

与门:所有输入全为真,输出为真;

或门:任意一个输入为真,则输出真;

非门:输出总是输入的反。


利用这三个门就可以做很多有趣的事情了。

你需要理解欧姆定律,理解电阻、电容、二极管、三极管/场效应管,理解与或非门电路,组合逻辑电路,时序逻辑电路,理解CPU和指令集,机器代码。

到了这一步,你就知道机器代码的一堆01010010011011是怎么通过控制单元,逻辑运算单元,寄存器,以及底下的加法器,编码器,译码器,多路选择器通过高电平低电平的脉冲跑起来的。

你还要理解汇编器,理解编译器和连接器,理解其中的词法分析,语法分析,代码生成,你需要理解操作系统。

到了这一步你就能明白电脑怎么讲你用高级语言编写的程序转换成机器代码的一堆01010010011011并提交给计算机执行的了。

涵盖这些知识的就是电路分析,模拟电路,逻辑电路,单片机,计算机组成原理,离散数学,数据结构,编译原理,操作系统这些计算机专业的课程。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

电脑怎样执行编程语言的? 的相关文章

  • C#中跨线程访问控件问题解决方案

    net 原则上禁止跨线程访问控件 xff0c 因为这样可能造成错误的发生 xff0c 推荐的解决方法是采用代理 用代理方法来间接操作不是同一线程创建的控件 第二种方法是禁止编译器对跨线程访问作检查 xff0c 可以实现访问 xff0c 但是
  • C#中Invoke的用法(转)

    转载 转自 xff1a http blog 3snews net html 30 34530 27563 html 在多线程编程中 xff0c 我们经常要在工作线程中去更新界面显示 xff0c 而在多线程中直接调用界面控件的方法是错误的做法
  • 【分析】浅谈C#中Control的Invoke与BeginInvoke在主副线程中的执行顺序和区别(SamWang)

    今天无意中看到有关Invoke和BeginInvoke的一些资料 xff0c 不太清楚它们之间的区别 所以花了点时间研究了下 据msdn中介绍 xff0c 它们最大的区别就是BeginInvoke属于异步执行的 Control Invoke
  • C#中Invoke 和 BeginInvoke的涵义和区别

    BeginInvoke 方法真的是新开一个线程进行异步调用吗 xff1f 参考以下代码 xff1a public delegate void treeinvoke private void UpdateTreeView MessageBox
  • C# 理解lock

    一 为什么要lock xff0c lock了什么 xff1f 当我们使用 线程 的时候 xff0c 效率最高的方式当然是 异步 xff0c 即各个线程同时运行 xff0c 其间不相互依赖和等待 但当不同的线程都需要访问某个资源的时候 xff
  • excel操作的几种方法

    using System using System Collections Generic using System Text using System Data using System Windows Forms using Syste
  • c#中如何实现拷贝对象

    大家都知道 xff0c 在C 中变量的存储分为值类型和引用类型两种 xff0c 而值类型和引用类型在数值变化是产生的后果是不一样的 xff0c 值类型我们可以轻松实现数值的拷贝 xff0c 那么引用类型呢 xff0c 在对象拷贝上存在着一定
  • 深入了解Windows句柄到底是什么

    总是有新入门的Windows程序员问我Windows的句柄到底是什么 xff0c 我说你把它看做一种类似指针的标识就行了 xff0c 但是显然这一答案不能让他们满意 xff0c 然后我说去问问度娘吧 xff0c 他们说不行网上的说法太多还难
  • 句柄概念

    句柄 xff08 handle xff09 xff0c 有多种意义 xff0c 其中第一种是指程序设计 xff0c 第二种是指Windows编程 现在大部分都是指程序设计 程序开发这类 第一种解释 xff1a 句柄是一种特殊的 智能指针 当
  • 腾讯云大数据发布最新产品矩阵,助力企业整合打通海量数据

    9月11日 xff0c 主题为 释放数字经济发展的新动能 的腾讯全球数字生态大会大数据专场在线上拉开帷幕 腾讯大数据领域的多位顶级专家 xff0c 与包括 Hadoop 创始人 Doug Cutting 在内的业内顶级大咖 xff0c 以及
  • C/C++中函数参数传递

    看了内存管理的有关内容 xff0c 有一点了解 xff0c 但不是很深入 xff0c 发现之前写代码时有很多细节问题没有注意到 xff0c 只知道这样做可以实现功能 xff0c 却不知道为什么可以这样 xff0c 对于采用自己的方法造成的隐
  • Windows窗口刷新机制详解

    1 Windows的窗口刷新管理 窗口句柄 xff08 HWND xff09 都是由操作系统内核管理的 xff0c 系统内部有一个z order序列 xff0c 记录着当前窗口从屏幕底部 xff08 假象的从屏幕到眼睛的方向 xff09 x
  • C语言中内存分布及程序运行中(BSS段、数据段、代码段、堆栈)

    BSS段 xff08 bss segment xff09 通常是指用来存放程序中 未初始化 的 全局变量 的一块内存区域 BSS是英文Block Started by Symbol的简称 BSS段属于静态内存分配 数据段 xff1a 数据段
  • C# 窗体Show和ShowDialog 方法的区别详解

    CenterParent 窗体在其父窗体中居中 CenterScreen 窗体在当前显示窗口中居中 xff0c 其尺寸在窗体大小中指定 Manual 窗体的位置由 Location 属性确定 WindowsDefaultBounds 窗体定
  • c# 窗口句柄问题 。

    1 如何获得一个窗口的句柄 xff1f 例如获取窗口PictureBox控件 xff08 其他控件也可以 xff09 的句柄 xff0c csharp view plain copy IntPtr handle 61 pictureBox
  • c#中已知一个外部窗口的句柄,怎么关闭

    已知一个外部窗口的句柄 xff0c 怎么关闭它 怎么给这个窗口的一个文本框设置内容 public void Test Handle windowhandle string TextBoxName System windws froms fr
  • 数组内存分配概念

    在这里解答一下 xff1a int arr 4 amp arr 1 61 arr 0 43 sizeof int 静态分配 xff0c 即普通数组 xff0c 由于在栈中分配 xff0c 而栈的生成方向是自高地址向低地址生成 所以有 xff
  • 静态数组和动态数组 内存分布

    数组是程序设计中是一个非常重要的概念 数组是一个用于收集大量类似数据的容器 xff0c 以及其每一个元素能被相同处理过程迭代来处理的一个抽象体 创建数组一般有三种方式 xff1a 全局 静态范围的数组 xff0c 局部变量数组 xff0c
  • C语言 内存分配 地址 指针 数组 参数 解析

    指针简介 指针式保存变量地址 的变量 增加阅读难度 指针 和 goto 语句会增加程序的理解难度 容易出现错误 ANSI C American National Standards Institute 美国国家标准学会 即标准C 通用指针类
  • C++数组与指针概念

    指向数组元素的指针 一个变量有地址 xff0c 一个数组包含若干元素 xff0c 每个数组元素都在内存中占用存储单元 xff0c 它们都有相应的地址 指针变量既然可以指向变量 xff0c 当然也可以指向数组元素 xff08 把某一元素的地址

随机推荐

  • [代码实例][C]Linux实现线程池

    ThreadPool h span class token macro property span class token directive keyword ifndef span THREADPOOL H span span class
  • C语言中指针的初始化和赋值

    1 指针的初始化 指针初始化时 xff0c 61 的右操作数必须为内存中数据的地址 xff0c 不可以是变量 xff0c 也不可以直接用整型地址值 但是int p 61 0 除外 xff0c 该语句表示指针为空 此时 xff0c p只是表示
  • Aspose.Cells使用总结大全

    使用到 Aspose Cells 插件 xff0c 整理一下 一 xff1a 新建解决方案 xff0c 目录如下 目录说明 xff1a Program cs 入口类 ExcelGenerator cs Aspose Cells 操作类 As
  • C#图像处理基础概念知识

    图像是人类获取和交换信息的主要来源 xff0c 因此 xff0c 图像处理的应用领域必然涉及到人类生活和工作的方方面面 随着人类活动范围的不断扩大 xff0c 图像处理的应用领域也将随之不断扩大 xff08 1 xff09 航天和航空技术方
  • c#中字节数组byte[]、图片image、流stream,字符串string、内存流MemoryStream、文件file,之间的转换

    字节数组byte 与图片image之间的转化 字节数组转换成图片 span class hljs keyword style color 0088 public span span class hljs keyword style colo
  • C/C++ 文件操作之CreateFile、ReadFile和WriteFile

    1 CreateFile 这个函数的功能是创建或者打开一个文件或者I O设备 xff0c 通常使用的I O形式有文件 文件流 目录 物理磁盘 卷 终端流等 如执行成功 xff0c 则返回文件句柄 INVALID HANDLE VALUE 表
  • String到底是值类型还是引用类型(C#)

    MSDN 中明确指出 String 是引用类型而不是值类型 xff0c 但 String 表面上用起来却像是值类型 xff0c 这又是什么原因呢 xff1f 首先从下面这个例子入手 xff1a span class hljs comment
  • VS2013 MFC基于对话框编程(创建工程)

    一 新建MFC项目 选择 xff1a 基于对话框MFC的使用 xff1a 在共享DLL中使用MFC xff08 程序运行需要dll xff09 在静态库中使用MFC xff08 程序较大 xff0c 运行时不需要dll xff09 设置MF
  • MFC中画直线和曲线的几种方法

    一 画直线 要想在MFC中画出有颜色的线条 xff0c 首先就要设置DC的画笔 xff0c 我们可以按如下方法来设置画笔 xff1a 第一步 xff1a 在View类中添加一个COLORREF类型的数据成员m Color xff08 用来保
  • C++函数的传入参数是指针的指针(**)的详解

    要修改变量的值 xff0c 需要使用变量类型的指针作为参数或者变量的引用 如果变量是一般类型的变量 xff0c 例如int xff0c 则需要使用int 类型的指针类型int 作为参数或者int的引用类型int amp 但是如果变量类型是指
  • 零基础学习WinCE开发

    在接触WinCE时候的基础 xff1a 软件语言基础 xff1a C C 43 43 C 我不是计算机专业的 xff0c 所以这些语言基础也是比较业余的 经验不丰富 xff0c 遇到问题就是查MSDN或者到网上查找相关解决方案即可 xff0
  • SD-WAN为什么备受欢迎?

    SD WAN即软件定义广域网 xff0c 通过集中控制器 xff0c 将广阔地理范围内的计算机 云服务 数据中心集中起来统一管理 比较多地用于企业组网场景 xff0c 那么为什SD WAN如此受欢迎呢 xff1f 近年来 xff0c 企业的
  • C#实现缩放和剪裁图片的方法示例

    C 实现缩放和剪裁图片的方法 分享给大家供大家参考 xff0c 具体如下 xff1a 1 2 3 4 5 6 7 8 9 10 11
  • 虚拟内存解疑

    虚拟内存别称 虚拟存储器 xff08 Virtual Memory xff09 电脑 中所运行的 程序均需经由 内存执行 xff0c 若执行的程序占用内存很大或很多 xff0c 则会导致内存消耗殆尽 为解决该问题 xff0c Windows
  • windows内存结构概述

    13 1 Windows的虚拟地址空间安排 13 1 1虚拟地址空间的分区 xff08 即虚拟地址空间布局 xff09 进程的地址空间划分 分区 x86 32位 Windows 3GB用户模式下的x86 32位Windows X64 64位
  • 变量名和内存地址及符号表

    1 变量名是给编译器看的 xff0c 编译器根据变量是局部还是全局分配内存地址或栈空间 xff0c 所谓的变量名在内存中不存在 xff0c 操作时转换成地址数存放在寄存器中了 其实可以理解为是符号表起到了连接作用 2 符号表 xff08 此
  • C/C++编译和链接过程详解 概述 (重定向表,导出符号表,未解决符号表)

    详解link 有 些人写C C 43 43 以下假定为C 43 43 程序 xff0c 对unresolved external link或者duplicated external simbol的错误信息不知所措 xff08 因为这样的错误
  • 编译器构造概述(详细)

    一 编译器简介 前面谈到静态链接器构造的基本流程 xff0c 最后提到所构造的链接器若要能正常工作的前提是需要构造一个能生成符合链接器输入文件格式的编译器 xff0c 本文构造一个符合这种具体格式要求编译器 但是编译器的直接编译的结果一般是
  • nm命令中符号类型详解

    nm命令介绍的很多 xff0c 但大多不介绍其函数符号标志的含义 最近在调试动态库时常用到 xff0c 其中用的最多的用法 nm A grep aaa c 43 43 filt A 为了显示文件 xff0c c 43 43 filt转换为可
  • 电脑怎样执行编程语言的?

    链接 xff1a https www zhihu com question 29227521 answer 154819061 来源 xff1a 知乎 著作权归作者所有 商业转载请联系作者获得授权 xff0c 非商业转载请注明出处 这个问题