電腦怎樣執行編程語言的?

不是說怎麼編寫程序,例如java,電腦是怎麼把java那一堆函數英文啥的轉換成命令並執行的?


這個問題真的是很大,讓我們自頂向下的解釋。

在頂層,程序員編寫出來的都是源代碼。源代碼可以使用各種高級語言寫成,例如 c/c++ c# java python 等等;也可以使用對應平台的低級語言寫成,例如彙編。想必你已經了解其中的過程了。

到這一步為止,距離最終機器可以執行的指令還有一大步要走。

首先要面臨的一個問題是:源代碼都是以人類語言寫成的。即便是能夠和機器指令一對一翻譯的彙編代碼,依然是人類語言。計算機無法理解其中的含義,所以不可能執行。

所以我們需要將人類語言翻譯為計算機語言。計算機能聽懂的語言,就叫做機器語言,簡稱機器碼。

在這裡說幾句題外話。

在計算機歷史的上古時代,大概是上個世紀50年代之前。那時編譯理論和形式語言還沒有得到發展。幾乎所有的程序都是直接由機器碼寫成的。比如由工程師直接將二進位機器碼和數值編寫在打孔卡上,通過讀卡機讀入計算機存儲器,然後執行。

而打孔卡長這個樣子:

(來自 wiki,80列標準 IBM 打孔卡,你能讀出上面是什麼意思嗎?)

計算機的基本架構雖然經過了將近百年的發展,但是核心的模型倒是一直很穩定,都是存儲程序模型。

首先將程序指令從外存(打孔卡,磁帶,硬碟,軟盤,光碟,快閃記憶體卡,網路等)讀入內存,然後讓處理器從內存按順序取指執行,結果寫回內存中。

在那個年代,人們對程序運行原理的理解是不存在什麼障礙的。工程師怎麼寫,計算機就嚴格的按照指令執行。每一條指令對應一個步驟。最後的到結果。

在這種條件下,程序開發絕對是頂尖的職業,首先能夠理解目標機的架構就需要相當的功夫了。其次還要按照機器的方式思考,寫出正確無誤的指令序列。

這樣的開發過程無疑限制了計算機行業的發展。

同時,即便是擅長於按照機器方式思考的工程師,也認為機器指令太難記了。如你所見,在打孔卡上準確無誤的寫上指令真是頭疼的要死。所以工程師們開發了一套助記符,用來指示對應的機器碼,這樣以來,程序的編寫和 debug 就方便多了。到上世紀40年代末期,就已經有一批成熟的助記符體系了。

(ARM v7 彙編指令卡中的某一頁)

關於助記符的話題,暫且擱置。

回到正題。為了將人類語言翻譯成機器變成機器能夠理解的語言,還需要進行翻譯。就好像你不懂英語,英語可以翻譯成漢語,這樣你就能明白其中的含義。對於計算機來說,這個過程是一樣的。不過計算機對於翻譯有更高的要求。人類之間互相翻譯語言,有一些微小的出入並不影響理解,計算機為了能夠準確的得到結果,要求這個翻譯的過程,必須保證「將一種語言翻譯成涵義相同的等價的另一種語言」。

在早期,程序的規模還比較小,翻譯的過程可以人工的進行。利用查表的方式,最終是可以得到等價的機器碼序列。隨著計算機科學的發展,程序規模膨脹的越來越快,人工翻譯變的沒有可行性。此時就有人提出,編寫一套軟體來進行這個翻譯的過程。

一開始人們只用彙編語言進行程序開發。所以只需要將彙編語言翻譯為機器語言就可以了。這是相當直截了當的過程,因為彙編語言的助記符和機器指令是一一對應的關係。所以只需要完成一個能夠自動查表並轉換的程序即可。很快,這樣的程序就被發明了出來。我們稱之為「彙編器」。

伴隨著彙編器的發展,工程師又開始想要偷懶。他們認為,既然彙編器可以將彙編指令翻譯成等價的機器碼,那麼在翻譯之前一定也可以做一些預先處理的工作,將一個助記符轉換為多個助記符組成的序列。這樣以來,開發人員就可以使用較少的代碼,寫出較多的內容。同時將常用的一些程序結構編寫成對應的助記符,在需要時就使用這個助記符,還可以幫助開發人員減少程序出錯的可能。簡直太好了。於是,人們又在彙編器中引入了宏指令。

所謂「宏(macro)」就是一套預先定義好的指令序列。每當彙編進行的時候,先預處理一次將宏等價的展開,然後再進行翻譯。如此,源程序變的更加容易理解了。

宏的引入,催生了程序結構化表達。在今天的彙編語言當中,我們也可以像使用高級語言的 if else for while 語句一樣,使用等價的結構語句。只不過,彙編中的結構語句都是宏實現的。

結構化表達給了一些計算機科學人員啟發。能不能更進一步,使用完全結構化,脫離某個對應機器平台的形式化語言來描述一個源程序?於是,就有了高級語言及其編譯器。

開發人員利用高級語言編寫程序,然後利用對應的編譯器生成中間代碼,最後再將中間代碼變成機器碼。中間代碼可以是等價的彙編代碼,也可以是其它類型的代碼例如 JVM 的位元組碼。最終處理中間代碼的程序可以是一個對應平台的彙編器,也可以是一個解釋器。在這裡姑且隱去這些細節,將編譯的最終產物都視為一系列可以被執行的二進位機器碼。關於編譯器的更多內容,在網上可以找到很多詳細的資料。在這個話題下,編譯器不是核心問題,我就不再深入討論了。

至此,就得到了一個可以被執行的程序了。這個文件的內容是一系列二進位指令和數據組成的序列。它能被裝入機器的內存,並且可以被處理器解碼執行。

但是,為什麼是二進位

說回來,計算機其實是長期使用的一個簡稱。嚴格的講應該叫做「電子計算機」。但是計算機的形態並不限於電子式計算機。算盤,計算尺,對數計算表都可以算作廣義上的計算機,同時在電子式計算機出現之前,它的還有一個機械式計算機的表親。

(來自 Wiki 。 查爾斯·巴貝奇 的分析機。蒸汽動力驅動,採用十進位,其內存能夠存儲1000個50位的十進位數,相當於20.7 KB 的 SRAM 或 DDRAM。採用打孔紙帶讀入程序,具有類似彙編語言的助記符編程系統,是圖靈完備的。很蒸汽朋克,嗯?)

可是我們並不認為算盤以及計算尺和現代計算機是同一個東西。最核心的區別在於,現代計算系統是可編程的。按照這個定義,上面的分析機也是現代電子是計算機的鼻祖。它身上的核心模型一直繼承至今。

在分析機上,已經實現了 「 存儲程序計算機 」。

這也就是現代計算系統的基本概念:

  1. 以運算單元為中心
  2. 採用存儲程序原理
  3. 存儲器是按地址訪問的,線性的空間
  4. 控制流由程序的指令流產生
  5. 指令由操作碼和操作數組成

這一概念所描述的計算模型具有以下的過程:將完整的程序裝入存儲器後,運算單元依照地址按順序的從存儲器中取出指令和數據且執行。指令序列就像流水一樣「流」入運算單元,當指令流盡,就意味著程序結束了。

對於計算機,自然是希望運算的速度越快越好。所以機械式運算很快就淘汰了。取而代之的就是電子式計算機。

電子式計算機的硬體基礎,就是數字電路。因為二進位可以很自然的表示開和關的兩種狀態,高和低的兩種狀態,通和斷的兩種狀態,等等。所以很快就取得了主導低位,其它進位的數字電子器件淪為小眾。

理論上,二進位和十進位表示的數的範圍是一樣多的。因為實數集是一個連續同,不同進位實質上是對數集的不同分割。

基於二進位數字電子器件製造的電子式計算機自然就需要二進位的輸入輸出。

到了這個層次,我們基本上解釋了高級語言源程序是如何成為計算機可以識別的二進位指令序列的。接下來的問題是,計算機如何識別並執行二進位指令呢?

通用處理器被稱為「通用」,就是因為它不限定於特定用途。路邊上買一個計算器。只能計算四則運算,而計算機還能進行字處理,可以玩遊戲看電影。都有賴於通用處理器提供的運算能力。

為了實現通用的目標,處理器在設計之初就不能對未來可能進行的運算進行限制。但是未來的可能性是無窮的。處理器不可能窮盡所有可能。

所以,處理器提供了一套它能夠支持的運算操作的集合,稱為「指令集」。指令集就限定了該處理器能夠進行的所有運算。而且這些運算通常都是有關於數字的運算。如果我們想解決一個任意問題,那麼首先要把這個問題轉換為一個數字問題,再把數字問題的解答過程,用指令集當中的指令求解。

將其它問題轉換為數學問題的一種方法就是編碼。比如我們常見的 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

為了便於解釋,這裡隱去了很多細節,並且使用了很多偽代碼。上面的彙編程序是不經修改是無法通過編譯的。

等價的二進位文件又是什麼樣子呢?為了方便閱讀,我稍稍整理了一下,並且加上了對應的彙編代碼,它長這個樣子:

(第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. 指令是如何執行的?

程序運行的過程,上面已經提到過了。程序是完整的裝入內存中的。運算器能夠直接操作的只有存儲器中的數據。他們之間的硬體連接如圖所示:

sorry,搞錯了,是這個:

圖上黃色的一根粗線其實以一排並列的導線,在這裡是 8 根導線並列在一起。只是看起來畫在了一起,其實是互相分開的。

使用 8051 及其外部擴展存儲器介面電路來說明問題主要是為了簡便。在不失準確性的前提下,我依然隱去一些細節,方便理解。

訪問存儲器的過程主要關注兩個問題:

  1. 送出地址
  2. 取回數據

考慮一般的訪問過程,當運算器執行如下操作時:

mov al, 0xD0D0

將會發生什麼呢?

首先 mov 指令指定了數據搬移的操作,第二個操作數是一個立即數參數,直接給出了地址。現在就要到存儲器當中去找這個地方了。

處理器不能直接操作存儲器的具體單元,但是它可以請存儲器將對應單元中的數據準備好,然後取回來。你肯定有過取快遞的經歷,菜鳥驛站去過吧,門市點不會讓你親自進倉庫去找快遞的,但是你可以告訴快遞小哥你的單號,然後他進去幫你找到,最後把包裹交給你。內存和這一個意思。

處理器首先將地址放到地址匯流排上,地址匯流排就是圖上 D0-D7 和 A8-A15 這15 根導線組成的。

處理器將自己的埠設置成對應的值,就把地址放到了匯流排上。0xD0D0對應的埠狀態應該是:

(圖有點小)

然後,處理器告訴存儲器,我準備好取數據了,地址在匯流排上,請你準備數據。具體的方式就是拉低 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 的操作方式就是如此。編程的方式是女工進機房去接插頭。

(假設我們有三條可編程指令流水線,那麼如果我們想依次執行數據轉移,異或,求和的操作,就需要連接 #1 的 move, #2 的 xor 和 #3 的 add)

而後出現了指令編碼。送入的指令被解碼器解碼後,自動啟動一組對應的電路。

這樣說也許很難讓人明白「自動」的含義。所以我在這裡實現一個簡單的編碼指令處理器。在這裡我們只實現 3 條指令:

  • 指令0:將輸入端的數據存入寄存器 a
  • 指令1:將輸入端的數據存入寄存器 b
  • 指令2:取寄存器 a 寄存器 b 中的值求和,將結果放入寄存器 a

在這裡我們只研究解碼,不管其它的因素,這樣就簡化了問題。不多說,直接上圖:

最頂上的 instraction register 和 instraction decoder 的部分就是指令解器。首先將指令讀入一個寄存器,然後解碼。實際的運算器也是這樣的流程。圖中藍色的就是數據匯流排,寄存器內的值分別是兩個寄存器 Qx 埠上的值。

讓我們啟動他它,來算一下 0x10 + 0x0F (16 + 15)是多少。

上電之後,我們注意到:

  • 寄存器 a 和寄存器 b (右下兩個)都被初始化為 0xFF
  • 輸入埠(左下角)上的值為 0x00
  • 指令寄存器(上方)當中當前的指令為 0x0F (15號),這是一條未定義的指令,所以沒有任何效果。

首先我們要執行

mov a, 0x10

當指令讀入後,在指令輸入端將會是 0x00 的狀態,解碼輸出埠上輸出全 0,指示出目前要執行0 號指令。同時選中了 0 號指令的執行電路。

數據端的輸入為 0001 0000。mov 命令的狀態下,輸入選擇器選擇輸入埠的數據放到匯流排上。同時,寄存器 a 被激活,將匯流排上的值存入:

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

第二條指令,我們將啟動寄存器 b,然後存入數據。指令為:

mov b, 0x0f

instraction decoder 的 1 號輸出被選中,此時激活了 1 號指令的電路。輸入端的 0x0F 被存入了寄存器 b。而寄存器 a 中的值保持不變。

第三條指令,2 號指令,求值。

add

沒有給出操作時是因為操作是已經隱含的指明了,就是 a 和 b:

解碼器的 2 號輸出選中了。全加器完成了運算(左側是第四位,右側是高四位),結果放上了匯流排,被鎖存到了加法器的輸出緩衝器當中。

同一時間,雙輸入選擇器也被激活。它截斷了輸入埠的連接而選擇加法器輸出緩衝值作為輸入,將其放上了匯流排。

寄存器 a 從匯流排取得數據,存入。完成了指令。

看看,結果是 0x1F,恰好就是我們預期的 31。

實際中的處理器的處理過程比這個複雜得多。這裡為了方便理解,做了很多簡化。但是概念都是相同的。處理器自動的從內存中讀入指令和數據,然後解碼,啟動對應的電路,最後拿到結果。如此往複。

到此為止,已經幾乎完全說明了計算機的運算原理,以及高級語言和機器語言的關係。但是我們依然可以更進一部,探究一下數字電路的構成。編碼器是怎麼運行的?寄存器是怎麼鎖存數據的?

上面一直在說解碼器,那麼解碼器到底是什麼?

處理器內部的指令解碼器可能非常複雜,也許是一個器件,也有可能是一組器件,或者是可編程的硬體電路(對的,硬體電路也是可以編程的,例如 FPGA)。

而這裡,我在上面的例子中使用的解碼器:74LS42 4 Lines to 10 Lines BCD to Decimal Decoder (4線10線BCD解碼器)的內部結構是這樣的:

可以看到,BCD 輸入端(左邊)輸入後首先連接了非門(NOT),然後進入一個選擇矩陣,最後通過三入與非門(NAND)輸出。

與門(AND)、或門(OR)、非門(NOT)是數字電路中,最基礎的三種邏輯門電路。它們的組後構建了大量的實用器件。

關於三種邏輯門,它們的特性可以使用真值表來表示:

(1 代表真,0 代表假)

與門:所有輸入全為真,輸出為真;

或門:任意一個輸入為真,則輸出真;

非門:輸出總是輸入的反。

利用這三個門就可以做很多有趣的事情了。


你需要理解歐姆定律,理解電阻、電容、二極體、三極體/場效應管,理解與或非門電路,組合邏輯電路,時序邏輯電路,理解CPU和指令集,機器代碼。

到了這一步,你就知道機器代碼的一堆01010010011011是怎麼通過控制單元,邏輯運算單元,寄存器,以及底下的加法器,編碼器,解碼器,多路選擇器通過高電平低電平的脈衝跑起來的。

你還要理解彙編器,理解編譯器和連接器,理解其中的詞法分析,語法分析,代碼生成,你需要理解操作系統。

到了這一步你就能明白電腦怎麼講你用高級語言編寫的程序轉換成機器代碼的一堆01010010011011並提交給計算機執行的了。

涵蓋這些知識的就是電路分析,模擬電路,邏輯電路,單片機,計算機組成原理,離散數學,數據結構,編譯原理,操作系統這些計算機專業的課程。

最後,可以參考我在另一個問題下的答案:

C#,C++,Java等語言具體是怎麼研究出來的? - 知乎


題主只問到指令層,那麼我簡單說一下。編譯部分

1,做詞法解析,把代碼分詞。

2,做模式匹配,生成語法樹。

3,根據語法樹生成位元組碼。對於java虛擬機而言,位元組碼就相當於具體的指令了。

4,java虛擬機執行位元組碼。相當於每個位元組碼都是由一段彙編組成,位元組碼其實就是針對彙編的抽象。

5,cpu執行彙編指令。這裡就是題主想知道的指令層了。


我的個神呀,你是想讓誰在這裡copy《編譯原理》這本書,或者《計算機原理》,或者《計算機史話》,或者……


你需要需要一本書,Noam Nisan和Shimon Schocken計算機系統要素 (豆瓣)

一個配套的網站,The Elements of Computing Systems

一個配套的coursera課程,Build a Modern Computer from First Principles: From Nand to Tetris (Project-Centered Course) - Hebrew University of Jerusalem | Coursera

接著就可以從布爾代數--Nand門--基本邏輯門--加法器--ALU,

D觸發器--寄存器--計數器--機器語言--內存--CPU,

彙編器--虛擬機--Jack語言--編譯器--操作系統.

加油!!!


這個學期在學相關內容,但是不太懂你具體想問哪一部分?

我這裡以c為例,不討論java。

一個程序的代碼需要編譯才能運行,編譯就是指從代碼到彙編語言的部分,簡單理解編譯器就是像翻譯一樣的工作。彙編代碼就是像:

add $t1,$t2,$t3 這樣,指令簡單,功能也簡單。

使用彙編語言的原因是因為高級語言如c太複雜計算機理解不了。

這些彙編語言之後會被編譯為二進位的指令碼,這些指令碼根據計算機不同而擁有不同長度。一個指令可以分為很多部分,每個部分有不同的功能。比如前8個數表示功能,再往後8個表示第一個寄存器等等。

這些彙編語言會被扔進cpu中作計算。計算機使用數字電路,所以只有開關兩種狀態,計算機用這兩種狀態完成整個計算過程,這部分就比較複雜了,每個操作都有一個邏輯電路可以展示。


根據我的理解。

微型電子計算機,也就是大家平時所說的電腦,它是不能理解編程語言的。編程語言這種東西其實是人類寫出來給人類看得,計算機不能理解。

而一台可以工作的計算機有非常多的組成部分,大體上可以分為存儲部分和計算部分以及輸入輸出部分。計算機的計算部分是計算機的核心,這個東西一般叫做CPU,是一塊超大規模集成電路。

而各種編程語言所編寫的程序最終都會被編譯,或者解釋成計算機能夠理解的指令。而指令集指的就是CPU能夠執行的所有指令的一個集合。所以編程語言就會有移植的問題,比如同樣用c語言編寫的程序,在這種CPU上運行可能沒有問題,但是在那種CPU上運行可能就會有問題。所以目前的CPU大體上都是有標準的,不能隨隨便便的製造。

至於一條又以一條的指令是怎麼執行的。現代的電子計算機,基本都是符合圖靈模型的。基本上就是輸入-》存-》算-》存-》輸出,這樣的模型。反正就是看不見摸不著,需要你有點想像力。

至於為什麼不能把人類的語言直接作為編程語言。這個原因就在於計算機太笨啦,我們不得不發明一種新的語言以便適應計算機的笨。

有人覺得編程語言好像比自己的語言還難學,那純粹是一種錯覺。因為人類自己學會自己的語言絕不是一件容易的事情。小孩子很早就能聽見聲音了,但是他自己能夠清晰的表達自己的意思,正常的孩子也得到5歲。而很多人直到死也不能熟練運用自己的語言去寫哪怕一本200頁的書,還不限體裁。寫個說明文可能很容易,但是寫的清楚明白不啰嗦,超過一半的人也做不到。而藝術性的寫作更難了,能出個詩集的,估計百萬分之一吧。

所以編程語言當然是很難學習的。這是正常情況,需要你很多年的努力,才能掌握一門編程語言。而且現在的編程還處在比較初級的階段,程序員還需要寫很多代碼來處理和業務無關的問題。而且計算機語言也沒有一個是萬能的,不同的情況下需要使用不同的編程語言。

總之編程以及理解機器的運行,並非那麼的容易,需要多年的學習於應用。


曾經我也不懂,但是學過編譯原理,自學過彙編後就不這麼想了。


補充一下, MHRD on Steam 可以讓大家較為方便快速的體驗從門到CPU的過程,雖然最後對指令集的介紹有點倉促

=============================

雖然已經有那麼多答案了,但決定還是寫一下,算是對四年本科的紀念吧。

自底向上寫,正好和課程順序一樣。

首先,有門課叫"電路基礎", 它對後續課程唯一的價值就是告訴你"導線可以導電"。

然後,有門課叫「數字邏輯」,它告訴我們有一種電子原件叫作與非門(NAND), 它像一個智能開關一樣可以根據若干條導線(輸入信號)的通斷決定某一條導線(輸出信號)的通斷。然後用這個門實現了一堆其它的門: 與或非抑或(and or not xor)...然後又實現了一些有具體功能的電路,比如加法器, 選擇器,解碼器...這些電路對於特定的輸入產生特定的輸出,稱為組合電路.

然後又引入了"時鐘信號"以及用於儲存的"觸發器". 在此之前的組合電路都只能實現某些特定的功能,但是加入了時鐘和存儲器從而搭出的時序電路才讓一切魔法了起來。至此我們已經學到了所有構建計算機必須的硬體知識,然而在這門課里,我們只學習了(簡單?)的狀態機應用。

後來,又來了門課叫作"體系結構",確切說是它附帶的「體系結構實驗」, 讓我們學會了如何用上面學到的時序電路製作一個 CPU(其實按照我的理解加上ram可以跑程序了直接理解成電腦也沒毛病). 而上面提到的MHRD on Steam 重現的基本就是數字邏輯+體系結構實驗(遊戲最後兩關)的內容(語法比VHDL和Verilog簡單太多所以坑也少很多雖然有時很蠢).

最後(雖然其實是在體系結構前學的),這門課叫"計算機原理",教材就是大名鼎鼎的 深入理解計算機系統 (豆瓣). 這本書的前半本就是告訴你怎麼給上面在體系結構實驗上倒騰出來的CPU寫程序(彙編)以及他們是怎麼運行的,以及怎麼高效運行(流水線)。稍微提了一些C語言和硬體的關係然而那主要是"編譯"和"操作系統"課講的東西.


提到電腦怎麼執行編程語言就繞不開機器語言,要說機器語言就要從電腦的發源開始說起。

首先題主可能已經知道了,通俗地來說,計算機在底層就是一堆元器件按照一定邏輯連接起來,各個器件在CPU的管理下按照二進位指令(舉個不嚴謹的例子,這個介面上電壓為高的話這個元件就做什麼事情,為低的話做另一件事情)完成一件一件任務的機器。這樣的指令我們稱為機器語言。

提到機器語言,那就不得不說世界上第一台電腦ENIAC了,就是那個佔了兩個房間幾十噸的東西。雖然今天看來它十分笨重性能低下,但是在當時也算是最先進的科技成果。不過給這台機器編程是一件非常累人的事情,因為在當時機器編程是採用撥開關的方式把程序對應的電路直接告訴計算機,相當於每一次編程都是對計算機的重新組建,可以想像如果我們不對這些開關進行調整,也就只能做它上一次被設置能做的事情。(就像圖中這樣調整開關序列)

這樣進行編程自然是很煩人的,於是我教祖師馮 諾依曼先生想出了一個絕妙的主意:把程序指令視作數據一併存在計算機的存儲器里,這樣的話就不用每次編程的時候重新構建一次計算機的電路了。

但是即使這樣,每次寫程序的時候還是要去寫那種看起來長成這樣:01010101 的機器語言,不僅難記而且容易出錯(有一個段子就是說某些上古程序員他們的鍵盤只有01兩個鍵,說的就是他們直接寫機器語言)。於是人們又在這個基礎上發展出了一種類似於助記符的彙編語言。它們長的像這個樣子:(圖為一種MIPS架構,架構可以理解成一種機器語言或者彙編語言指令集)

這樣程序員只需要寫好彙編,再由機器翻譯成為機器語言就可以運行了。但是這樣的彙編語言還是有兼容性的問題。能夠在一台計算機(例如使用Intel的x86架構)上順利運行的彙編程序,在另一台計算機(例如使用ARM或者MIPS架構)上就沒有辦法運行了,因為它們使用的指令集不一樣。

於是人們又在彙編語言的基礎上開發出了高級語言,也就是現在的C/C++,JAVA,Python等等語言,這樣的語言無論在什麼機器上語法都是一致的。等到寫完程序之後,在具體的機器上運行的時候,由編譯器把高級語言改成對應那台機器架構的彙編語言,再由彙編語言翻譯成機器語言,這樣一個程序就能夠在不同架構的計算機上運行了。

在高級語言之中還分為解釋型語言和編譯型語言,解釋型語言(例如Python)是在運行的時候由解釋器一條一條地把高級語言語句編程機器語言然後執行,解釋一條執行一條,所以速度要慢一些,而編譯型語言(例如C/C++)是把高級語言在運行之前先通過編譯器變成機器語言,這個過程稱為編譯過程,等到運行的時候直接運行機器語言就可以了,所以速度要快一些,而至於JAVA就比較不倫不類,雖然JAVA也有一個編譯的過程,但不是轉換為機器語言,而是轉換為一種JAVA獨有的位元組碼(你可以理解為JAVA自己的彙編語言)。


鑒於題主的問題很籠統,我想題主可能只想了解大概、並不想翻閱專業書籍,那這個答案就以門外漢為前提寫了,如有冒犯我會自行刪改答案的。

——————————————————————————

我們熟知的個人計算機里,我把它分成六層,從下到上為:
存儲器、CPU、邏輯電路等硬體

機器語言

操作系統

彙編語言

編譯器

高級語言

而我們的終極目標是:讓CPU通過電路調動儲存器和運算器動起來。(為什麼他們動起來就能成。是一位姓馮的偉大先知說的,有問題可以另開問題或者私信我)

硬體和機器語言:這些硬體都是用電驅動的,可以想像晶元里都在跑著電流,因為電流只有通電和不通電(其實是高電位和低電位,方便理解這裡就這麼寫了),通電錶示1、斷電錶示0,所以機械語言都是用0和1的二進位表示的,但0和1寫紙上的話會寫很長也不好分辨,我們在寫的時候把二進位轉換成十六進位(0100=8 。1110=E)。晶元里使用了特定電路、與門電路、非門電路、74181ALU等(《數字邏輯》、《計算機組成原理》):比如通電成(89D0)表示將寄存器1的內容寫入寄存器2。

【此處應該有圖片,但好像不會插入圖片,誰教我一下我給補上】

換句話說就是機械語言可以驅動計算機硬體,讓電腦動起來。

操作系統:

操作系統的功能包括管理計算機系統的硬體、軟體及數據資源,控制程序運行,改善人機界面,為其它應用軟體提供支持,讓計算機系統所有資源最大限度地發揮作用,提供各種形式的用戶界面,使用戶有一個好的工作環境,為其它軟體的開發提供必要的服務和相應的介面等。實際上,用戶是不用接觸操作系統的,操作系統管理著計算機硬體資源,同時按照應用程序的資源請求,分配資源,如:劃分CPU時間,內存空間的開闢,調用印表機等。

——百度百科

一個用戶程序的執行自始至終是在操作系統控制下進行的。一個用戶將他要解決的問題用某一種程序設計語言編寫了一個程序後就將該程序連同對它執行的要求輸入到計算機內,操作系統就根據要求控制這個用戶程序的執行直到結束。操作系統控制用戶的執行主要有以下一些內容:調入相應的編譯程序,將用某種程序設計語言編寫的源程序編譯成計算機可執行的目標程序,分配內存儲等資源將程序調入內存並啟動,按用戶指定的要求處理執行中出現的各種事件以及與操作員聯繫請示有關意外事件的處理等。

彙編器和彙編語言:這裡說他將彙編語言翻譯為機器語言的功能。這一步其實比較容易理解,比如MOV %edx,%eax翻譯成機器語言變成了89D0,就是把MOV翻譯成89(並不是簡單地翻譯,每個指令都不一定對應某個二進位數,但這裡就不討論了)。

總結一下就是彙編器把彙編語言翻譯成機器語言。

編譯器與高級語言:包括比較原始的C語言、C++,後來的比如題主說的java,C#等語言就屬於高級語言。他們各自的語法和框架(主推的編程套路),但目的都是用較少的、更易理解的方式表達彙編語言。高級語言出現的原因是彙編語言的承載能力太低,難懂。只算1+1還好,如果涉及到簡單的分支或循環,起碼都得寫個十幾行

以下為彙編語言:

movl 8(%ebp),%eax

movl 12(%ebp),%ecx

movl 16(%ebp),%edx

.L2:

addl %edx,%eax

imull %edx,%ecx

subl $1,%edx

testl %edx,%edx

jle .L5

cmpl %edx,%ecx

jl .L2

.L5

C語言:

int loop(int x,int y,int n)

{

do{

x+=n;

y*=n;

n--;

}while((n&>0)(y&return x;

}

彙編語言既難寫、又難懂,而C語言雖然屬於很早期的高級語言,比起彙編語言已經相當易懂(相當程度的接近英語)。編譯器就是將高級語言翻譯成彙編語言(《編譯原理》)。編譯器本身是可能就是由高級語言寫出來的(比如eclipse編譯java,而它本身也是用java寫的。)

這裡寫一下編譯的簡單過程(感覺不寫的話回答的太敷衍):

掃描程序;

語法分析;

語義分析;

優化程序;

代碼生成;

目標代碼。

對編譯器有興趣的話可以看一下這個專欄,很棒。

從零開始寫個編譯器吧系列 - 知乎專欄

編譯器負責:源程序→編譯器 →目標機器代碼程序

——————————————————————————————

最後總結一下,高級語言、彙編語言、機器語言,從易懂到晦澀,計算機領域的前輩們創造了越來越高級、簡單易懂的語言和工具,我們踩在他們的肩膀上才能創造出功能更加強大的程序。

計算機用編譯器把java編譯成彙編語言,再經過彙編器變為機器語言,操作系統調度計算機硬體,硬體結構通電最終實現了程序員的意志。

以上。如有冒犯、請聯繫我、我會自行刪改答案的。


哈哈,我來試著解決題主的困惑,並滿足一些小白們的好奇心~

首先題主的問題是「電腦怎樣執行編程語言的?」,問題補充是「 電腦是怎麼把java那一堆函數英文啥的轉換成命令並執行的 ?」 我相信這是很多小白都會有的困惑,在我沒學編程之前我也有這樣的困惑,代碼都是些看不懂的符號加字母,怎麼就變成各種各樣的程序的呢?別擔心,小葵花編程課堂開講啦~~

1. 計算機的工作原理

先來介紹一些計算機工作原理方面的知識,這些知識可以幫助我們理解編寫程序與運行該程序時最終會發生的事情的聯繫。

現代計算機可分幾個部件:

  • 中央處理器(CPU):擔負絕大部分的計算工作,負責處理程序;
  • 隨機存儲器(RAM,也就是內存):作為一個工作區來保存程序和文件;
  • 永久存儲器:一般是硬碟,即,使計算機關機後也能保留數據;
  • 輸入輸出設備(如鍵盤滑鼠,顯示屏):提供人與計算機之間的通信。

計算機的可編程性主要是指對中央處理器的編程,所以這裡集中討論下CPU。CPU的工作原理,簡單來說,就是從內存中獲取一個指令,執行,然後重複這兩個步驟,以驚人的速度來從事其枯燥的工作。CPU有自己的小工作區,該工作區由若干個寄存器(registers)組成,每個寄存器可以保存一個數。

這裡有兩個有趣的地方,首先,存儲在計算機中的一切內容都是數字。數字、字母、符號都是以數字形式存儲的,每個字元有一個數字代碼。計算機裝載到寄存器中的指令是以數字形式存儲的,指令集(CPU所能理解的有限指令)中每條指令具有一個數字代碼。其次,計算機程序最終必須以這種數字指令代碼(也就是機器語言,只有1和0)來表示。

理解了計算機運行方式後的一個結果就是:如果你希望計算機做某件事,就必須以一種計算機可以直接理解的語言(機器語言)提供一個特定的指令列表(一套程序)確切地告訴它你想要j它給你做的事及如何去做。在剛開始時你只能通過機器語言僅用1和0來表示指令,簡直就是反人類。隨著編程語言的發展,就先有了彙編語言,再到現在普遍使用的高級語言(C、C++、JAVA等等)也就是題主所說的一堆英文。

2. 高級計算機語言和編譯器

高級編程語言所帶來便利就是:首先,你不必用數字代碼表示指令。其次,你的代碼更接近自己思考解決問題的方式,而非計算機使用的操作步驟,從抽象的層次上表達程序猿的意念(233)。而且高級語言更易於學習。看到一行行的高級語言代碼,比如:

printf("hello,world!");

你可以很容易的理解它的作用,但是看到等價的用數字代碼表示若干指令的機器語言代碼,可就頭大了,跟看甲骨文似的。但問題在於,計算機和你恰好相反。對於計算機來說,高級指令是無法理解的胡言亂語。因此,就有了編譯器編譯器是將高級語言程序解釋成計算機可以直接理解的機器語言指令集的程序

3. 編程機制

(答主學識淺薄,在這裡只能用C語言作為例子解釋了)

學過C的小夥伴都知道,新建文件時必須為.c文件,也就是你的源代碼,裡面是你用C語言敲出來的代碼。將源代碼文件轉換為可執行文件,也就是包含可以運行的機器語言代碼的文件,在C中分為兩步完成:編譯鏈接。編譯器將源代碼轉換為中間文件,鏈接器將此中間代碼與其他代碼相結合生成可執行文件。

中間文件一般來說就是,通過編譯器,將源代碼轉換為機器語言代碼,再將結果放置在一個目標代碼文件中。在C中一般就是.obj格式的文件。雖然它包含機器語言代碼,但文件還不能運行,它還不是一個完整的程序。目標代碼文件缺少兩個元素。第一個元素是啟動代碼(start-up code),這個代碼相當於你的程序和操作系統之間的介面。第二個元素是庫常式的代碼。比如說你的c文件中的代碼使用了main() , printf() 這兩個函數。但目標代碼文件並沒有包含這一函數的代碼,它只包含聲明使用printf()函數的指令。實際代碼存儲在另一個稱為「庫」的文件中。在C中就是沒斷代碼前預處理的 #include& 中的 stdio.h ,意為標準輸入輸出頭文件(standard input/output header)。頭文件中包括了建立最終的可執行程序時編譯器需要用到的信息。例如,說明函數名以及函數如何使用。最後,鏈接器的作用就是將這三個元素(目標代碼、啟動代碼和庫代碼)結合在一起,並存放在單個文件中,即可執行文件,也就是.exe文件。

完成這整個過程所使用的,就是集成開發環境(IDE),可以通過它進行編輯,編譯和運行程序。

ps:在此發表一下對一些回答的看法,你所寫的是答案,而不是評論。

本文絕大部分內容參考C Primer Plus (豆瓣),給正在學語言的同學強勢安利。


計算機執行的是機器語言,先把問題問對了


你需要一本書,《揭開計算機的迷霧》


機器不懂語言,只懂指令,各種語言都要轉換成指令來運行。只是有的是直接,有的是間接。指令和架構有關,x86,x64,arm的都不一樣,但是語言可以一樣。


我以前寫的 一兩萬字,只有硬體原理,指令集原理,不含編譯原理。沒有公式。

初級計算機原理概論 第二次修訂.pdf


這樣跟你說吧,電腦其實就是一台機器,最開始它根本不認識我們現在所謂的各種程序語言,它只知道和電流打交道,電流有正負極之分,然後就有人把正負極兩個狀態標記為0和1,這時候電腦就認識0和1了。就可以用0和1來寫程序了。不過這時候程序都是存儲在紙帶上,用打洞和沒有洞來表示,然後執行紙帶就是執行程序。

不過這種辦法實在是太難受了,非常容易出錯。然後又有人把0和1組成不同的序列號,分別代表各個字母和符號,比如100代表字母d,101代表字母e,110代表字母n等,這樣電腦就認識26個字母和各個數字和符號了。這個時候就可以用26個字母和數字元號來代表各種程序段了,也可以存儲在磁帶或光碟上了。

然後很長一段時間大家都是這樣做,但是這樣的程序段實在太多太難記了,經過漫長的歲月,然後出來了彙編之父,重新定義了程序,將機器碼變成了指令,變數,循環,表達式,運算符等等各種程序邏輯。但是用彙編標準寫出來的程序必須通過彙編器編譯成機器碼後才可以執行。

但是彙編知識點太多,比較難掌握,就出來個C語音之父,在彙編語言的基礎上封裝了一層,自己重新定義了一套程序標準,讓程序更加人性化,更簡單易懂,寫起來也更加順手了。同樣,按照C語言標準寫出來的程序必須通過C語言編譯器編譯成彙編語言,然後再通過彙編器編譯成機器碼才能運行。不過這時候程序員們只需要學好C語言的標準就可以寫各種各樣的程序了,實在太方便了。所以一直到現在還有非常多的C語言程序員。

隨意互聯網的高速發展,越來越多的需求需要程序來實現,越來越多的軟體,網站,系統,APP等需要程序開發,這時候各種之父,比如JAVA之父,PHP之父等等根據自己的理解和業務需求又在C語言的基礎上封裝了一層,重新定義了一套自己的標準。使得程序學起來更加簡單,寫起來也更加溜,開發者也越來越多,而且這些年湧出了各種野生程序員,各種不合格的程序員,為什麼呢?因為程序沒那麼難學,沒那麼難懂了。但是用這些語言寫出來的程序也必須通過自己的編譯器編譯成C後,再編譯成彙編,最終編譯成機器碼後才能運行執行。

==========================================

說到這裡,你應該明白計算器程序是怎麼執行的了吧。可能我上面說的有些地方不太對,但是這樣說比較容易理解,用來回答這個問題或者回答普通人的疑問是比較好的一種方式。

經常有人問我這種問題,我都是這樣回答的,而且他們一聽就明白了,感覺發現了新世界一般,所以這裡也這樣回答一發。


《編碼的奧秘》


你可能需要學習,大學計算機專業開的以下課程

操作系統

面向對象的程序設計編譯原理

彙編語言程序設計

計算機組成原理

數字電子技術

模擬電子技術

電路基礎

......

可是我並不想知道從頭到尾的具體原理,學了這些我代碼還是寫不好呀!


推薦買本《計算機系統要素》,從與非門開始做起, 最後完成一個小小的計算機。買不到就買二手書^_^ 這本書特點就是簡單好學,學了有成就感。


推薦閱讀:

學計算機科學與技術電腦需要裝什麼東西?
知乎上面有哪些計算機科學與軟體工程方面值得一看的問答?
對大學選擇計算機科學類專業的女生說點什麼?
對計算機專業而言,計算機圖形學是否重要?
怎麼才能看懂計算機論文中的數學公式?

TAG:計算機 | 計算機科學 |