基于LLVM的編譯原理簡明教程 (1) - 寫編譯器越來越容易了

基于LLVM的編譯原理簡明教程 (1) - 寫編譯器越來越容易了

進入21世紀,新的編程語言如雨后春筍一樣不停地冒出來。需求當然是重要的驅動力量,但是在其中起了重要作用的就是工具鏈的改善。
2000年,UIUC的Chris Lattner主持開發了一套稱為LLVM(Low Level Virtual Machine)的編譯器工具庫套件。后來,LLVM的scope越來越大,Low Level Virtual Machine已經不足以表示LLVM的全部,于是,LLVM就變成了正式的名字。LLVM可以用于常規編譯器,JIT編譯器,匯編器,調試器,靜態分析工具等一系列跟編程語言相關的工作。
后來,Chris Lattner又主持開發了Clang,針對C/C++/Objective-C的前端。這個編譯器直接挑戰了GCC的統治地位。成為Apple系統的主要編譯器,在Android中,指名使用Clang的模塊也越來越多。
2012年,LLVM榮獲美國計算機學會ACM的軟件系統大獎,跟UNIX,WWW,TCP/IP,TeX,Java等經典系統作伴。
ACM系統獎完全名單

另外再八卦幾句LLVM的主要作者和架構師Chris Lattner。這哥們生于1978年。
2005年,Chris Lattner加入Apple。因為Apple對于GCC支持Objective-C不力的不滿,LLVM和Clang成為Apple替代GCC的殺手級武器。
2010年,Chris Lattner又開始主持開發Swift語言。

好了,言歸正傳。首先我們想說明的是,跟學院派的厚書給大家的印象不同,其實用LLVM寫個簡單的編譯器是件容易的事情,因為大部分事情LLVM都替我們做了。

用LLVM做個簡單的編譯器很容易

我們先看一個使用LLVM工具之后,實現一門編程語言的簡圖:

編譯器簡圖

完全需要我們手工,或者依靠其他工具如lex, yacc來做的事情,是從源代碼到token的詞法分析和從token到AST的語法分析。也就是前端的主要部分需要我們來實現,畢竟我們是這門語言的定義者。在介紹LLVM的書里,講前端的部分都是只占很小的篇幅的,所以大家可以take it easy.
在LLVM的萬花筒語言例子里,帶有注釋的詞法分析和語法分析也不過400行。大家如果覺得還復雜,后面我會帶大家做一些更簡單的,先完成一小部分功能,然后迭代式開發。區區百余行代碼,不需要學習編譯原理。
比如Clang就是一個實現了C/C++/Objective-C的前端。
從AST轉LLVM開始,LLVM就開始提供一系列的工具幫助我們快速開發。從IR(中間指令代碼)到DAG(有向無環圖)再到機器指令,針對常用的平臺,LLVM有完善的后端。也就是說,我們只要完成了到IR這一步,后面的工作我們就享有和Clang一樣的先進生產力了。
口說無憑,有例子為證,這是將二元表達式AST轉成IR的函數:

Value *BinaryExprAST::codegen() {
...
  switch (Op) {
  case '+':
    return Builder.CreateFAdd(L, R, "addtmp");
  case '-':
    return Builder.CreateFSub(L, R, "subtmp");
  case '*':
    return Builder.CreateFMul(L, R, "multmp");
  case '<':
    L = Builder.CreateFCmpULT(L, R, "cmptmp");
    // Convert bool 0/1 to double 0.0 or 1.0
    return Builder.CreateUIToFP(L, Type::getDoubleTy(TheContext), "booltmp");
  default:
...
  }
}

如何生成加減乘除的IR,在這個階段完全不用關心,LLVM會幫我們生成相應的代碼。
下面我們再看一個聲明函數原型的:

Function *PrototypeAST::codegen() {
  // Make the function type:  double(double,double) etc.
  std::vector<Type *> Doubles(Args.size(), Type::getDoubleTy(TheContext));
  FunctionType *FT =
      FunctionType::get(Type::getDoubleTy(TheContext), Doubles, false);

  Function *F =
      Function::Create(FT, Function::ExternalLinkage, Name, TheModule.get());

  // Set names for all arguments.
  unsigned Idx = 0;
  for (auto &Arg : F->args())
    Arg.setName(Args[Idx++]);

  return F;
}

詞法分析

在正則表達式已經成為基本技能的今天,詞法分析完全無門檻啊。正常情況下,我們只要寫一組正則表達式,或者寫個簡單的狀態機就可以了。

詞法分析的輸出是將源代碼解析成一個個的token。這些token就是有類型和值的一些小單元,比如是關鍵字,還是數字,還是標識符等等。這個階段不用管它們是如何組合的,都是干嘛的。
比如一個token類型是數值,值是3. 這個信息就已經足夠了,至于這個3干嘛用,后面整理AST的時候再放到合適的位置上去。
至于什么時上下文無關語言,什么是確定有窮自動機,非確定有窮自動機等等這些,暫時都不需要了解。

語法分析

語法分析誠然是比詞法分析要復雜一些。但是幸運的是,對于絕大多數語句和表達式來講,并不需要高深的知識,“移進-歸約”是個好方法,但是在我們學習的相當長的一段時期內都用不上。
語法分析的輸出是抽象語法樹AST,既然是棵樹,自然構造時需要遞歸。所以在大部分的語句中,我們只按遞歸下降的方法就足夠了。
對于表達式,遞歸下降還不夠用,至少運算符還有優先級啊。所以針對表達式,我們還需要運算符優先分析法。SLR,LALR和LR暫時還用不上。

語法制導翻譯和中間代碼生成

從前面的簡單例子中我們已經看到了,這部分大部分調用LLVM為我們提供的IR構造工具就可以了。入門階段我們能想到的,如代碼塊,函數調用,控制結構等,LLVM都為我們準備好了。

優化

LLVM主我們提供了大量的優化Pass供我們選擇和組合。在IR階段和機器碼階段,我們都將花大量的篇幅來討論優化。這可能也是我們真正感興趣的部分。

詞法分析很簡單

我們看一個官方的例子,首先定義token的類型,有一種算一種吧。將來擴展都是體力活。

enum Token {
  tok_eof = -1,
...
  // primary
  tok_identifier = -4,
  tok_number = -5
};

然后就是解析正則表達式,一點技術含量也沒有,哈哈~
我對官方的版本做了一點刪節,看起來可以更清楚一些:

static std::string IdentifierStr; // Filled in if tok_identifier
static double NumVal;             // Filled in if tok_number

/// gettok - Return the next token from standard input.
static int gettok() {
  static int LastChar = ' ';

  // Skip any whitespace.
  while (isspace(LastChar))
    LastChar = getchar();

  if (isalpha(LastChar)) { // identifier: [a-zA-Z][a-zA-Z0-9]*
    IdentifierStr = LastChar;
    while (isalnum((LastChar = getchar())))
      IdentifierStr += LastChar;
...
    return tok_identifier;
  }

  if (isdigit(LastChar) || LastChar == '.') { // Number: [0-9.]+
    std::string NumStr;
    do {
      NumStr += LastChar;
      LastChar = getchar();
    } while (isdigit(LastChar) || LastChar == '.');

    NumVal = strtod(NumStr.c_str(), nullptr);
    return tok_number;
  }
 ...
  // Check for end of file.  Don't eat the EOF.
  if (LastChar == EOF)
    return tok_eof;
...
}

如果不想手寫的話,lex, flex之類的工具很多,就是根據正則表達式來決定token類型,根據類型存一下對應的值。
如果token的類型多,就是搭積木,寫正則。都是體力活~

夠用的語法分析其實也很簡單

上面介紹了,我們自頂向下,構造抽象語法樹。
先定義個根類型吧:

/// ExprAST - Base class for all expression nodes.
class ExprAST {
public:
  virtual ~ExprAST() {}
  virtual Value *codegen() = 0;
};

我們先來個簡單的,就表示一個數字。這個好辦,就一個節點,存個數值。

/// NumberExprAST - Expression class for numeric literals like "1.0".
class NumberExprAST : public ExprAST {
  double Val;

public:
  NumberExprAST(double Val) : Val(Val) {}
  Value *codegen() override;
};

再來一個例子,變量,就是一個變量名么。賦值是下一步的事情了。

/// VariableExprAST - Expression class for referencing a variable, like "a".
class VariableExprAST : public ExprAST {
  std::string Name;

public:
  VariableExprAST(const std::string &Name) : Name(Name) {}
  Value *codegen() override;
};

函數原型:

/// PrototypeAST - This class represents the "prototype" for a function,
/// which captures its name, and its argument names (thus implicitly the number
/// of arguments the function takes).
class PrototypeAST {
  std::string Name;
  std::vector<std::string> Args;

public:
  PrototypeAST(const std::string &Name, std::vector<std::string> Args)
      : Name(Name), Args(std::move(Args)) {}
  Function *codegen();
  const std::string &getName() const { return Name; }
};

然后我們再看看如何通過token去構造一個數值的AST:
詞法分析時,已經把這個數值暫存了,我們把它拿來用就是了。

/// numberexpr ::= number
static std::unique_ptr<ExprAST> ParseNumberExpr() {
  auto Result = llvm::make_unique<NumberExprAST>(NumVal);
  getNextToken(); // consume the number
  return std::move(Result);
}

再看看函數聲明的:

/// prototype
///   ::= id '(' id* ')'
static std::unique_ptr<PrototypeAST> ParsePrototype() {
  if (CurTok != tok_identifier)
    return LogErrorP("Expected function name in prototype");

  std::string FnName = IdentifierStr;
  getNextToken();

  if (CurTok != '(')
    return LogErrorP("Expected '(' in prototype");

  std::vector<std::string> ArgNames;
  while (getNextToken() == tok_identifier)
    ArgNames.push_back(IdentifierStr);
  if (CurTok != ')')
    return LogErrorP("Expected ')' in prototype");

  // success.
  getNextToken(); // eat ')'.

  return llvm::make_unique<PrototypeAST>(FnName, std::move(ArgNames));
}

先讀函數名,再找左括號,然后是參數列表,最后是處理右括號。什么嘛,一點技術含量也沒有。。。

上面例子這些,都是沒有嵌套的,也不需要遞歸下降和算符優先。這些是處理比如二元表達式的時候才會遇到的。我們可以先學習容易的,先能把這些容易的組件組成一門雖然語言功能不全,但是真正實現了從源碼到機器指令的編譯器。

上面的例子都來自官方的例子萬花筒(Keleidoscope)語言的片段。官方教程當然寫得已經足夠好,但是還是稍嫌復雜了點,能生成一個可玩的編譯器的速度還是有點慢。我打算把學習曲線再降低一下,通過不斷地迭代,一點一點搭起可玩的編譯器,然后慢慢擴充功能。

Hello,LLVM

LLVM的下載

  1. 先下載LLVM
svn co http://llvm.org/svn/llvm-project/llvm/trunk llvm
  1. 在LLVM的tools目錄下,下載Clang(可選,但是建議):
cd llvm/tools
svn co http://llvm.org/svn/llvm-project/cfe/trunk clang
  1. 在LLVM的projects目錄下,可選下載compiler-rt,Libomp,libcxx,libcxxabi。反正我都下載了
svn co http://llvm.org/svn/llvm-project/compiler-rt/trunk compiler-rt
svn co http://llvm.org/svn/llvm-project/openmp/trunk openmp
svn co http://llvm.org/svn/llvm-project/libcxx/trunk libcxx
svn co http://llvm.org/svn/llvm-project/libcxxabi/trunk libcxxabi

LLVM的編譯

既然官方說大部分LLVM的開發者都使用Ninja,我們也就follow他們吧。
我在Mac下,所以使用Homebrew來安裝CMake和Ninja。Linux與些類似,GCC版本太舊之類的請自助。Windows我還沒試過,后面更新一下吧。

  1. 在LLVM目錄下創建build目錄
  2. cd build
  3. cmake -G Ninja
  4. Ninja

寫個LLVM上的Hello,World程序吧

從AST轉IR開始,我們都要用到LLVM的工具啦。先寫個小程序學習一下LLVM的程序是如何編譯的吧:

#include "llvm/IR/Module.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include <cstdlib>
#include <map>
#include <memory>
#include <string>
#include <vector>

static llvm::LLVMContext TheContext;
static llvm::IRBuilder<> Builder(TheContext);
static std::unique_ptr<llvm::Module> TheModule;
static std::map<std::string, llvm::Value *> NameValues;

int main(){
 TheModule = llvm::make_unique<llvm::Module>("hello,llvm",TheContext);

 TheModule -> dump();
 return 0;
}

輸出結果如下:

; ModuleID = 'hello,llvm'
source_filename = "hello,llvm"

如何鏈接LLVM的庫

使用LLVM庫的話,需要一大堆參數.
下面是在我的電腦上的參數:

-I/Users/ziyingliuziying/lusing/llvm/llvm/include -I/Users/ziyingliuziying/lusing/llvm/llvm/build/include  -fPIC -fvisibility-inlines-hidden -Wall -W -Wno-unused-parameter -Wwrite-strings -Wcast-qual -Wmissing-field-initializers -pedantic -Wno-long-long -Wcovered-switch-default -Wnon-virtual-dtor -Wdelete-non-virtual-dtor -Werror=date-time -std=c++11 -fcolor-diagnostics   -fno-exceptions -fno-rtti -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D__STDC_LIMIT_MACROS
-L/Users/ziyingliuziying/lusing/llvm/llvm/build/lib -Wl,-search_paths_first -Wl,-headerpad_max_install_names
-lLLVMCore -lLLVMSupport
-lcurses -lz -lm

每次都這么寫嚇死人了啊。于是LLVM為我們提供了llvm-config工具。剛才我那一大串,是用下面的命令行生成的:

llvm-config --cxxflags --ldflags --system-libs --libs core

完整的編譯命令可以這么寫:

clang++ -g toy.cpp `llvm-config --cxxflags --ldflags --system-libs --libs core` -o toy
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容