日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

[译] 理解编译器 —— 从人类的角度(版本 2)

發(fā)布時(shí)間:2025/3/8 编程问答 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 [译] 理解编译器 —— 从人类的角度(版本 2) 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
  • 原文地址:Understanding Compilers?—?For Humans (Version 2)
  • 原文作者:Luke Wilson
  • 譯文出自:掘金翻譯計(jì)劃
  • 本文永久鏈接:github.com/xitu/gold-m…
  • 譯者:Starrier
  • 校對(duì)者:Raoul1996, Gavin-Gong

理解編譯器 —— 從人類的角度(版本 2)

編程語(yǔ)言的工作原理

理解編譯器的內(nèi)部原理會(huì)促使你更高效地使用它。在這個(gè)按時(shí)間排序的概要中,了解編程語(yǔ)言和編譯器的工作原理。(為此)編寫了大量的鏈接、示例代碼和圖表來(lái)幫助你理解。


作者標(biāo)注

理解編譯器 —— 從人類的角度(Version 2)是我在 Medium 上發(fā)表的第二篇文章(有超過(guò) 21000 的閱讀量)的后續(xù)。我很高興自己的內(nèi)容對(duì)大家產(chǎn)生了積極的影響,我也很開(kāi)心能根據(jù)從原文章收集到的意見(jiàn)來(lái)對(duì)其進(jìn)行完整的重寫

  • 理解編譯器 —— 從人類的角度:盡管你知道點(diǎn)擊綠色按鈕就可以執(zhí)行,但你真的知道它的底層發(fā)生了哪些事情么?

我選擇 Rust 作為這篇文章的首選語(yǔ)言。因?yàn)樗敿?xì)、高效、現(xiàn)代化,而且從設(shè)計(jì)上看,編寫編譯器時(shí)會(huì)相對(duì)簡(jiǎn)單。我非常喜歡它。www.rust-lang.org/

寫這篇文章的目的是為了保證讀者能集中精神,而不是 20 頁(yè)的精神疲憊閱讀。你可以在文中的許多鏈接中,選擇自己感興趣的內(nèi)容,去了解相關(guān)內(nèi)容的深層解讀。當(dāng)然,大部分都是鏈接向維基百科的。

請(qǐng)隨意在文末進(jìn)行評(píng)論,或者說(shuō)出問(wèn)題建議。感謝你的關(guān)注,希望你可以喜歡這篇文章。


簡(jiǎn)介

什么是編譯器

當(dāng)然,你也可以認(rèn)為編程語(yǔ)言就是叫做編譯器的軟件,它讀取文本文件,對(duì)其進(jìn)行大量的處理,然后生成二進(jìn)制文件。由于計(jì)算機(jī)只能讀 1 和 0,比起二進(jìn)制,人類更擅長(zhǎng)寫 Rust,所以編譯器將人類可讀的文本轉(zhuǎn)化為計(jì)算機(jī)可讀的機(jī)器代碼。**

編譯器可以是將一個(gè)文本轉(zhuǎn)變成另一個(gè)文本的程序。比如,這里有用 Rust 編寫的編譯器,它將 0 與 1 相互轉(zhuǎn)化:

// 一個(gè)示例編譯器,將 0 與 1 互換。fn main() {let input = "1 0 1 A 1 0 1 3";// 對(duì)輸入的每個(gè)字符 `c` 進(jìn)行迭代let output: String = input.chars().map(|c|if c == '1' { '0' }else if c == '0' { '1' }else { c } // 如果不是 0 或 1,就忽略它(不進(jìn)行處理)).collect();println!("{}", output); // 0 1 0 A 0 1 0 3 } 復(fù)制代碼

盡管這個(gè)編譯器不讀取文件,不生成 AST(抽象語(yǔ)法樹(shù))或者二進(jìn)制文件,但它仍然被看成是一個(gè)編譯器,原因很簡(jiǎn)單,就是它可以翻譯輸入的內(nèi)容。

編譯器會(huì)做什么事情

簡(jiǎn)而言之,編譯器讀取源代碼并生產(chǎn)二進(jìn)制文件。由于直接從人類可讀的復(fù)雜代碼轉(zhuǎn)換成 1 和 0 非常復(fù)雜,因此編譯器在運(yùn)行之前會(huì)有幾個(gè)處理步驟:

  • 讀取給定源代碼的每個(gè)字符。
  • 將字符排序?yàn)樽帜浮?shù)字、符號(hào)和運(yùn)算符。
  • 獲取已排序的字符,通過(guò)將它們與已有模式匹配并創(chuàng)建操作樹(shù)來(lái)確定它們要執(zhí)行的操作。
  • 迭代上一步生成的樹(shù)中的每一個(gè)操作,并生成等效的二進(jìn)制文件。
  • 雖然我說(shuō)編譯器會(huì)立即從運(yùn)算符樹(shù)轉(zhuǎn)換為二進(jìn)制,但它實(shí)際上會(huì)生成匯編代碼,然后組裝/編譯成二進(jìn)制代碼,匯編是一個(gè)更高層次的、人類可讀的二進(jìn)制文件。更多程序集的相關(guān)閱讀可在此查詢。

    解釋器是什么

    解釋器更像是編譯器,因?yàn)樗鼈兌甲x取一種語(yǔ)言,然后對(duì)其進(jìn)行處理。但是解釋器會(huì)跳過(guò)代碼生成,即時(shí)生成 AST。對(duì)解釋器來(lái)說(shuō),最大的優(yōu)點(diǎn)就是降低在調(diào)試運(yùn)行期間所花費(fèi)時(shí)間。編譯器在執(zhí)行前可能需要從一秒鐘到幾分鐘的時(shí)間來(lái)編譯程序,而解釋器則會(huì)立即開(kāi)始執(zhí)行,而不需要編譯。解釋器最大的缺點(diǎn)是需要在程序執(zhí)行之前安裝在用戶的計(jì)算機(jī)上。

    本文主要涉及編譯器,但應(yīng)該清楚它們之間的區(qū)別以及編譯器之間的關(guān)系。

    1. 詞法分析

    第一步是將輸入的內(nèi)容分割成字符。這一步稱為詞法分析,或標(biāo)記化。主要目的是將字符組合在一起,形成我們的單詞、標(biāo)識(shí)符、符號(hào)等。詞法分析通常不處理任何邏輯上的問(wèn)題,比如求解 2+2?——?它只會(huì)說(shuō)有三個(gè)標(biāo)記:一個(gè)數(shù)字:2,一個(gè)加號(hào),以及另一個(gè)數(shù)字:2。

    假設(shè)你是在給像 12+3 這樣的字符串下定義:它會(huì)讀取字符 1、2、+ 和 3。我們有單獨(dú)的字符,但我們必須將它們組合在一起;這是 tokenizer 的主要任務(wù)之一。比如,盡管我們將 1 和 2 最為單獨(dú)的字母,但我們最后還是要將它們組合在一起,然后解析成一個(gè)整數(shù)。+ 將被識(shí)別為一個(gè)加號(hào),而不是它的字面量值 —— 字符碼 43。

    如果你可以看到代碼并以這種方式使其更具意義,那么以下的 Rust 標(biāo)記生成器可以將數(shù)字分成 32 位整數(shù),并加上符號(hào)作為 Token 值 Plus。

    Rust Playground:play.rust-lang.org

    你可以單擊 Rust Playground 左上角的 “RUn” 按鈕,在你的瀏覽器中編譯并執(zhí)行代碼。

    在編程語(yǔ)言的編譯器中,lexer 詞法分析器可能需要幾種不同類型的標(biāo)記。例如,符號(hào)、數(shù)字、標(biāo)識(shí)符、字符串、運(yùn)算符等。這完全取決于語(yǔ)言本身是否知道你需要從源碼中提取什么樣的標(biāo)記。

    int main() {int a;int b;a = b = 4;return a - b; }掃描生成內(nèi)容: [Keyword(Int), Id("main"), Symbol(LParen), Symbol(RParen), Symbol(LBrace), Keyword(Int), Id("a"), Symbol(Semicolon), Keyword(Int), Id("b"), Symbol(Semicolon), Id("a"), Operator(Assignment), Id("b"), Operator(Assignment), Integer(4), Symbol(Semicolon), Keyword(Return), Id("a"), Operator(Minus), Id("b"), Symbol(Semicolon), Symbol(RBrace)] 復(fù)制代碼

    已進(jìn)行詞法分析的 C 源碼示例及其標(biāo)記。

    2. 解析

    解析器確實(shí)是語(yǔ)法的核心。解析器獲取由 lexer 生成的標(biāo)記,試圖查看它們是否在某些模式中,然后將這些模式與諸如調(diào)用函數(shù)、回調(diào)變量或者數(shù)學(xué)操作相關(guān)聯(lián)。解析器實(shí)際上定義了語(yǔ)言的語(yǔ)法。

    在解析器中,詞組 int a = 3 和 a: int = 3 之間的區(qū)別。解析器決定了語(yǔ)法的外觀。它確保括號(hào)和大括號(hào)的平衡性,每個(gè)語(yǔ)句都以分號(hào)結(jié)尾,而且每個(gè)函數(shù)都有一個(gè)名稱。當(dāng)代碼不符合順序,標(biāo)記與預(yù)期模式不符,解析器都會(huì)知道。

    有幾種不同的類型解析器可以編寫。其中最常見(jiàn)的一種是自頂向下的 recursive-descent 解析器。Recursive-descent 解析器使用和理解起來(lái)都是最簡(jiǎn)單的方法。我創(chuàng)建的所有解析器示例都是基于 recursive-descent。

    解析器解析的語(yǔ)法可以使用語(yǔ)法進(jìn)行概括。像 EBNF 這樣的語(yǔ)法可以描述像 12+3 這樣簡(jiǎn)單數(shù)字操作的解析器:

    expr = additive_expr ; additive_expr = term, ('+' | '-'), term ; term = number ; 復(fù)制代碼

    用于簡(jiǎn)單加減表達(dá)式的 EBNF 語(yǔ)法。

    請(qǐng)記住,語(yǔ)法文件不是解析器,但是它是解析器所做工作的概要。你可以圍繞這樣的語(yǔ)法構(gòu)建一個(gè)解析器。它將被人類使用,并且比直接查看解析器的代碼更容易閱讀和理解。

    該語(yǔ)法的解析器是 expr 解析器,因?yàn)樗琼敿?jí)內(nèi)容,所以基本上所有的內(nèi)容都與之相關(guān)。唯一有效的輸入必須是任意數(shù)字之間的加減。expr 期望 additive_expr 出現(xiàn)的主要是進(jìn)行加減的地方。additive_expr 首先期望一個(gè) term(一個(gè)數(shù)字),然后對(duì)另一個(gè) term 進(jìn)行加減。

    解析 12 + 3 而生產(chǎn)的示例 AST。

    解析器在解析過(guò)程生成的樹(shù)稱為抽象語(yǔ)法樹(shù),或者 AST。AST 擁有所有的操作。解析器不計(jì)算操作,只保證按正確的順序記錄它們。

    我將它們添加到之前的 lexer 代碼中,這樣就可以匹配我們的語(yǔ)法,并且可以像圖表一樣生成 AST。我用注釋 // BEGIN PARSER // 和 // END PARSER // 標(biāo)記了新解析代碼的開(kāi)頭和結(jié)尾。

    Rust 頁(yè)面:play.rust-lang.org

    事實(shí)上我們可以了解的更深入。假設(shè)我們想要支持僅僅是沒(méi)有運(yùn)算符的數(shù)字輸入,或者添加乘法和除法,甚至是添加優(yōu)先級(jí)。這可以快速更改語(yǔ)法文件,并進(jìn)行調(diào)整以將其反映在我們的解析器代碼中。

    expr = additive_expr ; additive_expr = multiplicative_expr, { ('+' | '-'), multiplicative_expr } ; multiplicative_expr = term, { ("*" | "/"), term } ; term = number ; 復(fù)制代碼

    新語(yǔ)法。

    Rust 頁(yè)面:play.rust-lang.org

    C 的掃描器(又名詞法分析器)和解釋器示例。從字符 "if(net>0.0)total+=net*(1.0+tax/100.0);" 開(kāi)始,掃描器組成一系列標(biāo)記,并為每個(gè)標(biāo)記分類,例如,作為標(biāo)識(shí)符、保留字。數(shù)字文字或運(yùn)算符。后一個(gè)序列由解析器轉(zhuǎn)化為語(yǔ)法樹(shù),然后由其余的編譯器階段處理。掃描器和解析器分別處理 C 語(yǔ)法中正常和適當(dāng)上下文無(wú)關(guān)的部分。Credit:Jochen Burghardt。原件。

    3. 生成代碼

    代碼生成器接受 AST,然后在代碼或匯編中生成等效的代碼代碼生成器必須以遞歸下降的順序遍歷 AST 中的每一項(xiàng) —— 就像解析器的工作原理 —— 然后在代碼中發(fā)出等效項(xiàng)。

    • 編譯器資源管理器 —— Rust (rustc 1.29.0):godbolt.org

    如果打開(kāi)上面的鏈接,你會(huì)看到左邊示例代碼生成的程序集。匯編代碼的第 3 行和第 4 行顯示了編譯器在 AST 中遇到常量時(shí)是如果生成常量代碼的。

    Godbolt 編譯器管理資源是一個(gè)優(yōu)秀的工具,允許你用高級(jí)語(yǔ)言編寫代碼并查看其生產(chǎn)的匯編代碼。你可以隨意查看這些,看看應(yīng)該編寫什么樣的代碼,但不要忘記將優(yōu)化標(biāo)志添加到語(yǔ)言的編譯器中,看看它們有多高明(Rust 的 -O)。

    如果你對(duì)編譯器如何在 ASM 中將局部變量保存到內(nèi)存中感興趣,這篇文章(“代碼生成”部分)詳細(xì)解釋了棧。在變量不是本地變量的多數(shù)情況下,高級(jí)編譯器將在堆上的為變量分配內(nèi)存,并將它們存儲(chǔ)在堆中而不是棧中。你可以在 StackOverflow 上關(guān)于存儲(chǔ)變量的信息。

    由于組裝是一個(gè)完全不同的復(fù)雜主題,所以我不會(huì)詳細(xì)討論它。我只想強(qiáng)調(diào)代碼生成器的重要性以及工作原理。此外,代碼生成器可以產(chǎn)生的不僅僅是集合內(nèi)容。Haxe 編譯器有一個(gè) backend,可以生成六種以上不同的編程語(yǔ)言;包括 C++、Java 和 Python。

    后端主要是編譯器的代碼生成器或計(jì)算程序;因此,前端是 lexer 和解析器。還有一個(gè)與優(yōu)化相關(guān)的中間件。IRs 將在本節(jié)末解釋。后端大部分與前端無(wú)關(guān),它只關(guān)心接收到的 AST。這意味著可以為幾種不同的前端或語(yǔ)言重用相同的后端。GNU 編譯器集合就是這種情況。

    我想,我再也找不大比我的 C 編譯器生成的后端代碼更好的示例了:你應(yīng)該可以找到。

    生成程序集之后,應(yīng)該將其寫入一個(gè)新的組裝文件(.s 或 .asm)。然后匯編器(程序集的編譯器)會(huì)傳遞該文件,并以二進(jìn)制形式生成等效的文件。二進(jìn)制代碼會(huì)寫入一個(gè)稱為對(duì)象文件(.o)的新文件。

    **對(duì)象文件是機(jī)器碼,它們是不可執(zhí)行的。**想讓它們成為可執(zhí)行文件,就需要將對(duì)象文件鏈接在一起。鏈接器接受這個(gè)通用的機(jī)器代碼,并使它們成為一個(gè)可執(zhí)行文件,一個(gè)共享庫(kù) 或 靜態(tài)庫(kù)。更多鏈接器可在此查詢。

    鏈接器是基于操作系統(tǒng)變化而來(lái)實(shí)用性程序。一個(gè)獨(dú)立的第三方鏈接器應(yīng)該可以編譯后端生成的對(duì)象代碼。在生成編譯器時(shí),不再需要?jiǎng)?chuàng)建自己的鏈接器。

    編譯器可能有一個(gè)代理中間件,或者是 IR。IR 是為優(yōu)化或翻譯成另一種語(yǔ)言而無(wú)損失的原生指令的表示。IR 不是源代碼,IR 是為了在代碼中發(fā)現(xiàn)優(yōu)化的可能性而進(jìn)行的無(wú)損簡(jiǎn)化。展開(kāi)循環(huán)和向量化是使用 IR 完成的。更多 IR 相關(guān)的優(yōu)化示例可在此 PDF 參閱。

    結(jié)論

    在你了解編譯器之后,你的編程開(kāi)發(fā)將會(huì)更加高效。將來(lái)的某一刻,你也許會(huì)對(duì)創(chuàng)造自己的編程語(yǔ)言感興趣。希望這篇文章能對(duì)你有所幫助。

    資源和深入學(xué)習(xí)的相關(guān)文章

    • craftinginterpreters.com/?——?指導(dǎo)你使用 C 和 Java 編寫編譯器。
    • norasandler.com/2017/11/29/…?—— ?對(duì)我來(lái)說(shuō),可能是最好的“編寫編譯器”教程。
    • 我的 C 編譯器和科學(xué)計(jì)算器解析器在這里以及這里。
    • 另一種稱為 precedence climbing 解析器的示例,可以在這里找到。Credit:Wesley Norris。

    如果發(fā)現(xiàn)譯文存在錯(cuò)誤或其他需要改進(jìn)的地方,歡迎到 掘金翻譯計(jì)劃 對(duì)譯文進(jìn)行修改并 PR,也可獲得相應(yīng)獎(jiǎng)勵(lì)積分。文章開(kāi)頭的 本文永久鏈接 即為本文在 GitHub 上的 MarkDown 鏈接。


    掘金翻譯計(jì)劃 是一個(gè)翻譯優(yōu)質(zhì)互聯(lián)網(wǎng)技術(shù)文章的社區(qū),文章來(lái)源為 掘金 上的英文分享文章。內(nèi)容覆蓋 Android、iOS、前端、后端、區(qū)塊鏈、產(chǎn)品、設(shè)計(jì)、人工智能等領(lǐng)域,想要查看更多優(yōu)質(zhì)譯文請(qǐng)持續(xù)關(guān)注 掘金翻譯計(jì)劃、官方微博、知乎專欄。

    總結(jié)

    以上是生活随笔為你收集整理的[译] 理解编译器 —— 从人类的角度(版本 2)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。