├── .gitignore ├── LICENSE ├── Learn-LLVM-12.tex ├── README.md ├── content ├── 1 │ ├── Part-1.tex │ ├── chapter1 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ └── 4.tex │ ├── chapter2 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── images │ │ │ ├── 1.jpg │ │ │ ├── 2.jpg │ │ │ └── 3.jpg │ └── chapter3 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── 7.tex ├── 2 │ ├── Part-2.tex │ ├── chapter4 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ ├── 9.tex │ │ └── images │ │ │ └── 1.jpg │ ├── chapter5 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── images │ │ │ └── 1.jpg │ ├── chapter6 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── images │ │ │ ├── 1.jpg │ │ │ ├── 2.jpg │ │ │ ├── 3.jpg │ │ │ ├── 4.jpg │ │ │ └── 5.jpg │ ├── chapter7 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ └── 5.tex │ └── chapter8 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ └── 6.tex ├── 3 │ ├── Part-3.tex │ ├── chapter10 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ └── 5.tex │ ├── chapter11 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ └── images │ │ │ ├── 1.jpg │ │ │ ├── 2.jpg │ │ │ ├── 3.jpg │ │ │ └── 4.jpg │ ├── chapter12 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 10.tex │ │ ├── 11.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ ├── 7.tex │ │ ├── 8.tex │ │ └── 9.tex │ └── chapter9 │ │ ├── 0.tex │ │ ├── 1.tex │ │ ├── 2.tex │ │ ├── 3.tex │ │ ├── 4.tex │ │ ├── 5.tex │ │ ├── 6.tex │ │ └── images │ │ └── 1.jpg └── Preface.tex └── cover.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | *.aux 3 | *.toc 4 | *.synctex.gz 5 | *.out 6 | *.log 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learn LLVM 12 2 | A beginner's guide to learning LLVM compiler tools and core libraries with C++ 3 | 4 | (*使用C++学习LLVM编译器和核心库的初学者教程*) 5 | 6 | * 作者:Kai Nacke 7 | 8 | * 译者:陈晓伟 9 | 10 | * 原文发布时间:2021年5月28日 (来源亚马逊) 11 | 12 | > 翻译是译者用自己的思想,换一种语言,对原作者想法的重新阐释。鉴于我的学识所限,误解和错译在所难免。如果你能买到本书的原版,且有能力阅读英文,请直接去读原文。因为与之相较,我的译文可能根本不值得一读。 13 | > 14 | >

— 云风,程序员修炼之道第2版译者

15 | 16 | ## 本书概述 17 | 18 | 学习如何构建和使用编译器,包括前端、流水线优化和利用LLVM核心库的强大功能构建新的后端编译器。 19 | 20 | LLVM是为了弥合编译器理论和实际开发之间的差异而出现的。它提供了模块化的代码库和先进的工具,帮助开发人员轻松地构建编译器。本书提供了对LLVM的介绍,帮助读者在各种情况下构建和使用编译器。 21 | 22 | 本书将从配置、构建和安装LLVM库、工具和外部项目开始。接着,向您介绍LLVM的设计,以及在每个编译器阶段(前端、优化器和后端)的实际工作方式。以实际编程语言为例,学习如何使用LLVM开发前端编译器,并生成LLVM IR,将其交给优化流水线,并从中生成机器码。后面的章节将展示如何扩展LLVM,以及LLVM中的指令选择是如何工作的。在了解如何为LLVM开发新的后端编译器之前,将重点讨论即时编译问题和LLVM提供的JIT编译的支持情况。 23 | 24 | 阅读本书后,您将获得使用LLVM编译器开发框架的实际经验,并得到一些具有帮助性的实际示例和源代码片段。 25 | 26 | #### 关键特性 27 | 28 | - 学习如何有效地使用LLVM 29 | - 理解LLVM编译器的高级设计,并将原则应用到自己的编译器中 30 | - 使用基于编译器的工具来提高C++项目的代码质量 31 | 32 | #### 内容纲要 33 | 34 | - 配置、编译和安装LLVM框架 35 | - 理解LLVM源码的结构 36 | - 了解在项目中可以使用LLVM做什么 37 | - 探索编译器是如何构造的,并实现一个小型编译器 38 | - 为通用源语言构造生成LLVM IR 39 | - 建立优化流水线,并根据自己的需要进行调整 40 | - 使用转换通道和clang工具对LLVM进行扩展 41 | - 添加新的机器指令和完整的后端编译器 42 | 43 | 44 | 45 | ## 作者简介 46 | 47 | **Kai Nacke**是一名专业IT架构师,目前居住在加拿大多伦多。毕业于德国多特蒙德技术大学的计算机科学专业。他关于通用哈希函数的毕业论文,被评为最佳论文。 48 | 49 | 他在IT行业工作超过20年,在业务和企业应用程序的开发和架构方面有丰富的经验。他在研发一个基于LLVM/Clang的编译器。 50 | 51 | 几年来,他一直是LDC(基于LLVM的D语言编译器)的维护者。在Packt出版过《D Web Development》一书,他也曾在自由和开源软件开发者欧洲会议(FOSDEM)的LLVM开发者室做过演讲。 52 | 53 | 54 | 55 | ## 审评者介绍 56 | 57 | **Suyog Sarda**是一名专业的软件工程师和开源爱好者,专注于编译器开发和编译器工具,是LLVM开源社区的积极贡献者。他毕业于了印度浦那工程学院,具有计算机技术学士学位。Suyog还参与了ARM和X86架构的代码性能改进,一直是Tizen项目编译团队的一员,对编译器开发的兴趣在于代码优化和向量化。之前,他写过一本关于LLVM的书,名为《LLVM Cookbook》,由Packt出版。除了编译器,Suyog还对Linux内核开发感兴趣。他在迪拜Birla Institute of Technology的2012年IEEE Proceedings of the International Conference on Cloud Computing, Technologies, Applications, and Management上发表了一篇题为《VM pin and Page Coloring Secure Co-resident Virtualization in Multicore Systems》的技术论文。 58 | 59 | 60 | 61 | ## 本书相关 62 | 63 | * github翻译地址:https://github.com/xiaoweiChen/Learn-LLVM-12 64 | * 本书代码:https://github.com/PacktPublishing/Learn-LLVM-12 65 | * 译文的LaTeX 环境配置:https://www.cnblogs.com/1625--H/p/11524968.html 66 | 67 | -------------------------------------------------------------------------------- /content/1/Part-1.tex: -------------------------------------------------------------------------------- 1 | 2 | 本节中,您将学习如何自己编译LLVM,以及如何根据您的需要调整构建。您将了解LLVM项目是如何组织的,并将利用LLVM创建您的第一个项目。您还将学习如何为不同的CPU架构使用LLVM,编译LLVM和应用程序。最后,您将探索编译器的结构,并创建一个小型编译器。\par 3 | 4 | 本节包括以下几章:\par 5 | 6 | \begin{itemize} 7 | \item 第1章,安装LLVM 8 | \item 第2章,浏览LLVM 9 | \item 第3章,编译器结构 10 | \end{itemize} 11 | 12 | \newpage 13 | 14 | -------------------------------------------------------------------------------- /content/1/chapter1/0.tex: -------------------------------------------------------------------------------- 1 | 2 | 要学习如何使用LLVM,最好先从源代码开始编译LLVM。LLVM是一个伞形项目,它的GitHub存储库包含了属于LLVM的所有项目的源代码。每个LLVM项目都位于存储库的顶级目录中。除了克隆存储库之外,您的系统还必须安装构建系统所需的所有工具。\par 3 | 4 | 本章中,您将学习到以下内容:\par 5 | 6 | \begin{itemize} 7 | \item 准备好环境,并展示如何设置构建系统。 8 | \item 使用CMake构建,这将包括如何编译和安装LLVM核心库以及使用CMake和Ninja编译和安装Clang。 9 | \item 定制化构建过程,了解影响构建过程的各种方式。 10 | \end{itemize} 11 | 12 | 13 | -------------------------------------------------------------------------------- /content/1/chapter1/1.tex: -------------------------------------------------------------------------------- 1 | 要使用LLVM,您的系统必须为常见的操作系统,如Linux、FreeBSD、macOS或Windows。使用Debug构建LLVM和Clang很需要数十GB的磁盘空间,因此要确保您的系统有足够的可用磁盘空间,至少应该有30GB的空余空间。\par 2 | 3 | 所需的磁盘空间很大程度上取决于所选的构建选项。仅以发布模式构建LLVM核心库,而且只针对一个平台,需要大约2GB的空余空间,这是最低需求。为了减少编译时间,一个高速的CPU(比如2.5GHz时钟速度的四核CPU)和一块SSD硬盘也会很有帮助。\par 4 | 5 | 甚至可以在小设备上构建LLVM,比如树莓派——只是需要花费很多时间。我在笔记本电脑上开发了本书的示例,这台笔记本电脑配备了2.7GHz的英特尔四核CPU,具有40GB RAM和2.5TB SSD硬盘空间。\par 6 | 7 | 您的开发系统必须安装一些必要的软件。让我们了解一下这些软件包的最低要求。\par 8 | 9 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black, title=Note] 10 | Linux发行版通常包含可以使用的最新版本,版本号为LLVM 12。LLVM的最新版本可能需要这里提到的软件包的最新版本。 11 | \end{tcolorbox} 12 | 13 | 从GitHub下载源代码,需要git(\url{https://git-scm.com/}),对版本没有要求。GitHub帮助页面推荐使用1.17.10以上版本。\par 14 | 15 | LLVM项目使用CMake(\url{https://cmake.org/})作为构建文件生成器,至少为3.13.4。CMake可以为各种构建系统生成构建文件。本书中,使用Ninja(\url{https://ninja-build.org/}),是因为它编译速度快,适用于所有平台,建议使用最新版本1.9.0。\par 16 | 17 | 您还需要一个C/C++编译器。LLVM项目是用现代C++编写的,基于C++14标准。需要符合标准的编译器和标准库。下面的编译器可以编译LLVM 12:\par 18 | 19 | \begin{itemize} 20 | \item gcc 5.1.0或更高版本 21 | \item Clang 3.5或更高版本 22 | \item Apple Clang 6.0或更高版本 23 | \item Visual Studio 2017或更高版本 24 | \end{itemize} 25 | 26 | 请注意,随着LLVM项目的开发,对编译器的需求很可能会发生变化。撰写本文时,有人讨论使用C++17,并放弃对Visual Studio 2017的支持。所以,应该使用系统可用的最新编译器版本。\par 27 | 28 | Python(\url{https://python.org/})用于生成构建文件并运行测试套件,版本至少应该是3.6。\par 29 | 30 | 虽然在本书没有涉及到Makefile,但可能有一些原因需要使用Make而不是Ninja。这种情况下,您需要使用GNU Make(\url{https://www.gnu.org/software/make/}) 3.79或更高版本。这两种构建工具的用法非常相似。对于这里的场景,用make替换命令中的ninja就可以了。\par 31 | 32 | 要安装必备软件,最简单的方法是使用操作系统中的包管理器。下面将为主流的操作系统安装软件准备相应的命令行。\par 33 | 34 | \hspace*{\fill} \par %插入空行 35 | \textbf{Ubuntu} 36 | 37 | Ubuntu 20.04使用APT包管理器。大多数基础设施已经安装完毕,只缺少开发工具。输入以下命令:\par 38 | 39 | \begin{tcolorbox}[colback=white,colframe=black] 40 | \$ sudo apt install –y gcc g++ git cmake ninja-build 41 | \end{tcolorbox} 42 | 43 | \hspace*{\fill} \par %插入空行 44 | \textbf{Fedora和RedHat} 45 | 46 | Fedora 33和RedHat Enterprise Linux 8.3的包管理器称为DNF。和Ubuntu一样,大多数基本实用程序都已经安装好了。输入以下命令:\par 47 | 48 | \begin{tcolorbox}[colback=white,colframe=black] 49 | \$ sudo dnf install –y gcc gcc-c++ git cmake ninja-build 50 | \end{tcolorbox} 51 | 52 | \hspace*{\fill} \par %插入空行 53 | \textbf{FreeBSD} 54 | 55 | 在FreeBSD 12或更高版本上,您必须使用PKG包管理器。FreeBSD与基于linux的系统的不同之处在于,Clang是首选编译器。输入以下命令:\par 56 | 57 | \begin{tcolorbox}[colback=white,colframe=black] 58 | \$ sudo pkg install –y clang git cmake ninja 59 | \end{tcolorbox} 60 | 61 | \hspace*{\fill} \par %插入空行 62 | \textbf{OS X} 63 | 64 | 对于OS X上的开发,最好从Apple商店安装Xcode。虽然本书中没有使用XCode IDE,但它附带了所需的C/C++编译器和实用程序。要安装其他工具,可以使用Homebrew软件包管理器(\url{https://brew.sh/})。输入以下命令:\par 65 | 66 | \begin{tcolorbox}[colback=white,colframe=black] 67 | \$ brew install git cmake ninja 68 | \end{tcolorbox} 69 | 70 | \hspace*{\fill} \par %插入空行 71 | \textbf{Windows} 72 | 73 | 和OS X一样,Windows没有包管理器。安装所有软件的最简单方法是使用Chocolately(\url{https://chocolatey.org/})包管理器。输入以下命令:\par 74 | 75 | \begin{tcolorbox}[colback=white,colframe=black] 76 | \$ choco install visualstudio2019buildtools cmake ninja git\ 77 | gzip bzip2 gnuwin32-coreutils.install 78 | \end{tcolorbox} 79 | 80 | 请注意,这只安装来自Visual Studio 2019的构建工具。如果你想获得Community Edition(包含IDE),那么你必须安装visualstudio2019community包而不是visualstudio2019 buildtools。Visual Studio 2019安装的一部分是VS 2019的x64 Native Tools命令提示符。使用此命令提示符时,编译器将自动添加到搜索路径中。\par 81 | 82 | \hspace*{\fill} \par %插入空行 83 | \textbf{配置Git} 84 | 85 | LLVM项目使用Git进行版本控制。如果没有使用过Git,那么应该先做一些Git的基本配置;也就是说,设置用户名和电子邮件地址。如果提交更改,将使用这两条信息。以下命令中,将Jane替换为您的姓名,将jane@email.org替换为您的电子邮件:\par 86 | 87 | \begin{tcolorbox}[colback=white,colframe=black] 88 | \$ git config \verb|--|global user.email "jane@email.org" 89 | \end{tcolorbox} 90 | 91 | \begin{tcolorbox}[colback=white,colframe=black] 92 | \$ git config \verb|--|global user.name "Jane" 93 | \end{tcolorbox} 94 | 95 | 通常情况下,Git使用vi编辑器提交消息。如果您更喜欢使用另一种编辑器,可以以类似的方式更改配置,例如:要使用nano编辑器:\par 96 | 97 | \begin{tcolorbox}[colback=white,colframe=black] 98 | \$ git config \verb|--|global core.editor nano 99 | \end{tcolorbox} 100 | 101 | 关于git的更多信息,请参阅由Packt出版的另一本书,《Git Version Control Cookbook - Second Edition》(\url{https://www.packtpub.com/product/git-version-control-cookbook/9781782168454})。\par 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /content/1/chapter1/2.tex: -------------------------------------------------------------------------------- 1 | 准备好构建工具后,就可以从GitHub签出所有的LLVM项目。所有平台上执行此操作的命令基本相同。但在Windows上,建议关闭对行结束符的自动转译。\par 2 | 3 | 我们分三部分来回顾这个过程:克隆存储库、创建构建目录和生成构建系统文件。\par 4 | 5 | \hspace*{\fill} \par %插入空行 6 | \textbf{克隆代码库} 7 | 8 | 在所有非Windows平台上,输入以下命令克隆代码库:\par 9 | 10 | \begin{tcolorbox}[colback=white,colframe=black] 11 | \$ git clone https://github.com/llvm/llvm-project.git 12 | \end{tcolorbox} 13 | 14 | 在Windows上,必须添加选项以禁用自动转译行结束符。在这里输入以下内容:\par 15 | 16 | \begin{tcolorbox}[colback=white,colframe=black] 17 | \$ git clone \verb|--|config core.autocrlf=false https://github.com/llvm/llvm-project.git 18 | \end{tcolorbox} 19 | 20 | 这将最新的源代码从GitHub克隆到一个名为llvm-project的本地目录中。现在,进入llvm-project目录:\par 21 | 22 | \begin{tcolorbox}[colback=white,colframe=black] 23 | \$ cd llvm-project 24 | \end{tcolorbox} 25 | 26 | 这个目录包含所有的LLVM项目,每个项目都有自己的目录。最值得注意的是,LLVM核心库位于LLVM子目录中。LLVM项目使用分支来进行后续版本开发(“release/12.x”)和标记(“llvmorg-12.0.0”)来标记某个版本。通过前面的clone命令,可以获得当前的开发状态。本书使用LLVM 12。要查看LLVM 12的第一个版本,输入以下命令:\par 27 | 28 | \begin{tcolorbox}[colback=white,colframe=black] 29 | \$ git checkout -b llvmorg-12.0.0 30 | \end{tcolorbox} 31 | 32 | 这样,就克隆了整个存储库,检出到对应的标记。\par 33 | 34 | Git还允许只克隆一个分支或标记(包括历史记录)。使用git clone \verb|--|branch llvmorg-12.0.0 https://github.com/llvm/llvm-project。使用-depth=1选项,可以防止历史信息的克隆。这节省了时间和空间,但显然也限制了你能在本地可以做什么。\par 35 | 36 | 下一步就是创建构建目录。\par 37 | 38 | \hspace*{\fill} \par %插入空行 39 | \textbf{创建构建目录} 40 | 41 | 与许多其他项目不同,LLVM不支持内联构建,需要单独的构建目录。可以在llvm-project目录中创建一个目录。先进入llvm-project目录:\par 42 | 43 | \begin{tcolorbox}[colback=white,colframe=black] 44 | \$ cd llvm-project 45 | \end{tcolorbox} 46 | 47 | 然后,为了简单起见,创建一个名为build的构建目录。Unix和Windows系统的命令是不同,在类Unix系统上,应该使用以下命令:\par 48 | 49 | \begin{tcolorbox}[colback=white,colframe=black] 50 | \$ mkdir build 51 | \end{tcolorbox} 52 | 53 | 在Windows上,应该使用以下命令:\par 54 | 55 | \begin{tcolorbox}[colback=white,colframe=black] 56 | \$ md build 57 | \end{tcolorbox} 58 | 59 | 然后,切换到构建目录:\par 60 | 61 | \begin{tcolorbox}[colback=white,colframe=black] 62 | \$ cd build 63 | \end{tcolorbox} 64 | 65 | 现在,您可以在这个目录中使用CMake工具创建构建系统文件了。\par 66 | 67 | \hspace*{\fill} \par %插入空行 68 | \textbf{生成构建系统文件} 69 | 70 | 要生成将使用Ninja编译LLVM和Clang的构建系统文件,请运行以下命令:\par 71 | 72 | \begin{tcolorbox}[colback=white,colframe=black] 73 | \$ cmake –G Ninja -DLLVM\underline{~}ENABLE\underline{~}PROJECTS=clang ../llvm 74 | \end{tcolorbox} 75 | 76 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black,title=Tip] 77 | 在Windows上,反斜杠字符$\setminus$是目录名分隔符,CMake会自动将Unix分隔符/转换为Windows分隔符。 78 | \end{tcolorbox} 79 | 80 | -G选项告诉CMake要为哪个系统生成构建文件。最常用的选项如下:\par 81 | 82 | \begin{itemize} 83 | \item Ninja: 对应Ninja的构建系统 84 | \item Unix Makefiles: 对应GNU Make 85 | \item Visual Studio 15 VS2017和Visual Studio 16 VS2019: 对应Visual Studio和MS Build 86 | \item Xcode: 对应Xcode工程 87 | \end{itemize} 88 | 89 | 可以使用-D选项设置各种变量来影响生成过程。通常,以CMAKE\underline{~}(由CMAKE定义)或LLVM\underline{~}(由LLVM定义)作为前缀。使用LLVM\underline{~}ENABLE\underline{~}PROJECTS=clang变量设置,CMake为LLVM之外的Clang生成构建文件。命令的最后一部分告诉CMake在哪里可以找到LLVM核心库源代码。下一节中会有更多的相关内容。\par 90 | 91 | 当生成了构建文件,LLVM和Clang可以用以下命令编译:\par 92 | 93 | \begin{tcolorbox}[colback=white,colframe=black] 94 | \$ ninja 95 | \end{tcolorbox} 96 | 97 | 根据硬件资源的不同,该命令的运行时间在15分钟(具有大量CPU内核、内存和快速存储的服务器)到数小时(内存有限的双核Windows笔记本)之间。默认情况下,Ninja使用了所有可用的CPU核。这有利于提高编译速度,但可能会阻止其他任务的运行。例如,在Windows笔记本上,Ninja在运行时几乎不能上网。幸运的是,可以使用-j选项限制资源的使用。\par 98 | 99 | 假设您有四个可用的CPU核,而Ninja应该只使用两个(因为有并行任务要运行)。在这里,应该使用以下命令进行编译:\par 100 | 101 | \begin{tcolorbox}[colback=white,colframe=black] 102 | \$ ninja –j2 103 | \end{tcolorbox} 104 | 105 | 当编译完成,可以运行测试套件,以检查是否一切正常:\par 106 | 107 | \begin{tcolorbox}[colback=white,colframe=black] 108 | \$ ninja check-all 109 | \end{tcolorbox} 110 | 111 | 同样,该命令的运行时因可用硬件资源的不同而有很大差异。Ninja检查目标运行所有测试用例,为每个包含测试用例的目录生成目标。使用check-llvm(而不是check-all)是运行LLVM测试,而不是Clang测试,check-llvm-codegen只运行来自LLVM的CodeGen目录中的测试(即llvm/test/CodeGen目录)。\par 112 | 113 | 也可以做一个快速的手动检查。使用的LLVM的llc,即LLVM编译器。如果使用-version选项,会显示它的LLVM版本,主机CPU,以及它所支持的所有架构:\par 114 | 115 | \begin{tcolorbox}[colback=white,colframe=black] 116 | \$ bin/llc -version 117 | \end{tcolorbox} 118 | 119 | 如果您在编译LLVM时有困难,那么可以参考LLVM系统文档入门中的常见问题部分(\url{https://llvm.org/docs/GettingStarted.html\#common-problems}),以获得常见问题的解决方案。\par 120 | 121 | 最后,安装可执行文件:\par 122 | 123 | \begin{tcolorbox}[colback=white,colframe=black] 124 | \$ ninja install 125 | \end{tcolorbox} 126 | 127 | 在类Unix系统上,安装目录是/usr/local。在Windows下,使用C:$\setminus$Program Files$\setminus$LLVM。当然可以修改,下一节将说明如何操作。\par 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /content/1/chapter1/3.tex: -------------------------------------------------------------------------------- 1 | CMake系统使用CMakeLists.txt文件对项目进行描述。顶层文件在llvm目录中,也就是llvm/CMakeLists.txt。其他目录还包含CMakeLists.txt,在构建文件生成期间会递归地包含这些文件。\par 2 | 3 | 根据项目描述中提供的信息,CMake检查已经安装了哪些编译器,检测库和符号,并创建构建系统文件,如build.ninja或Makefile(取决于选择的生成器)。还可以定义可重用的模块,例如检测LLVM是否已安装的函数。这些脚本被放置在特殊的cmake目录(llvm/cmake)中,在生成过程中会自动搜索该目录。\par 4 | 5 | 构建过程可以通过定义CMake变量来定制。命令行选项-D将为一个变量设置值,这些变量会在CMake脚本中使用。CMake自己定义的变量几乎总是以CMake\underline{~}为前缀,这些变量可以在所有项目中使用。由LLVM定义的变量前缀为LLVM\underline{~},但只能在项目定义中包含LLVM时使用。\par 6 | 7 | \hspace*{\fill} \par %插入空行 8 | \textbf{CMake定义的变量} 9 | 10 | 有些变量是用环境变量的值初始化的。最值得注意的是CC和CXX,它们定义了用于构建的C和C++编译器。CMake尝试使用当前的shell搜索路径自动定位C和C++编译器,并选择找到的第一个编译器。如果你安装了多个编译器,比如:gcc和Clang或不同版本的Clang,那么默认找到的可能不是预期构建LLVM的编译器。\par 11 | 12 | 假设您想使用clang9作为C编译器,使用clang++9作为C++编译器。可以在Unix shell中使用CMake:\par 13 | 14 | \begin{tcolorbox}[colback=white,colframe=black] 15 | \$ CC=clang9 CXX=clang++9 cmake ../llvm 16 | \end{tcolorbox} 17 | 18 | 它会设置cmake调用的环境变量的值。如果需要,可以为编译器指定绝对路径。\par 19 | 20 | CC是CMAKE\underline{~}C\underline{~}COMPILER cmake变量的默认值,而CXX是CMAKE\underline{~}CXX\underline{~}COMPILER cmake变量的默认值。您可以直接设置CMake变量,而不使用环境变量。这与前面的调用相同:\par 21 | 22 | \begin{tcolorbox}[colback=white,colframe=black] 23 | \$ cmake –DCMAKE\underline{~}C\underline{~}COMPILER=clang9$\setminus$\\ 24 | \hspace*{1cm}-DCMAKE\underline{~}CXX\underline{~}COMPILER=clang++9 ../llvm 25 | \end{tcolorbox} 26 | 27 | CMake定义的其他常用变量如下:\par 28 | 29 | \begin{itemize} 30 | \item CMAKE\underline{~}INSTALL\underline{~}PREFIX:在安装过程中添加到每个路径上的路径前缀。Unix上默认为\allowbreak /usr/local,Windows上默认为C:$\setminus$Program Files$\setminus$。如果要在/opt/LLVM目录下安装LLVM,必须指定-DCMAKE\underline{~}INSTALL\underline{~}PREFIX=/opt/LLVM。可执行文件复制到/opt/llvm/bin,库文件复制到/opt/llvm/lib,以此类推。 31 | 32 | \item CMAKE\underline{~}BUILD\underline{~}TYPE:不同类型的构建需要不同的设置,例如:调试构建需要指定用于生成调试符号的选项,并且通常是针对系统库的调试版本进行链接。相比之下,发布版本使用针对库的生产版本的优化标志和链接。此变量仅用于只能处理一种构建类型的构建系统,如Ninja或Make。对于IDE构建系统,必须使用IDE的机制在构建类型之间进行切换。可能的值如下:\par 33 | DEBUG: 使用调试符号构建\par 34 | RELEASE: 以速度优化为主的构建\par 35 | RELWITHDEBINFO: 使用调试符号的发布构建\par 36 | MINSIZEREL: 以优化生成文件大小为主的构建\par 37 | 默认的构建类型是DEBUG。要构建为发布版本,必须指定 -DCMAKE\underline{~}BUILD\underline{~}TYPE=RELE\allowbreak ASE。\par 38 | 39 | \item CMAKE\underline{~}C\underline{~}FLAGS和CMAKE\underline{~} FLAGS:当我们编译C和C++源文件时,这些是额外的标志。初始值取自CFLAGS和CXXFLAGS环境变量,可以替代变量使用。 40 | 41 | \item CMAKE\underline{~}MODULE\underline{~}PATH:指定在CMAKE模块中搜索的附加目录。在搜索默认目录之前搜索指定的目录,以分号分隔的目录列表。 42 | 43 | \item PYTHON\underline{~}EXECUTABLE:如果没有找到PYTHON解释器,或者如果安装了多个版本的PYTHON解释器。在CMake选择了错误的解释器时,可以将该变量设置为正确PYTHON二进制文件的路径。这个变量只有在包含了CMake的Python模块时才会生效(对于LLVM也是如此)。 44 | \end{itemize} 45 | 46 | CMake为变量提供了内置帮助。\verb|--|help-variable var选项打印var变量的帮助信息。例如,您可以输入以下命令来获取CMAKE\underline{~}BUILD\underline{~}TYPE的帮助:\par 47 | 48 | \begin{tcolorbox}[colback=white,colframe=black] 49 | \$ cmake \verb|--|help-variable CMAKE\underline{~}BUILD\underline{~}TYPE 50 | \end{tcolorbox} 51 | 52 | 也可以用下面的命令列出所有的变量(这个清单很长):\par 53 | 54 | \begin{tcolorbox}[colback=white,colframe=black] 55 | \$ cmake \verb|--|help-variablelist 56 | \end{tcolorbox} 57 | 58 | \hspace*{\fill} \par %插入空行 59 | \textbf{LLVM定义的变量} 60 | 61 | LLVM定义的变量的工作方式与CMake定义的变量相同,但没有内置帮助。常用的变量如下:\par 62 | 63 | \begin{itemize} 64 | \item LLVM\underline{~}TARGETS\underline{~}TO\underline{~}BUILD: LLVM支持不同的CPU架构。默认情况下,构建所有目标。使用此变量指定要构建的目标列表,由分号分隔。目前支持的目标有AArch64、AMDGPU、ARM、BPF、Hexagon、Lanai、Mips、MSP430、NVPTX、PowerPC、RISCV、Sparc、SystemZ、WebAssembly、X86、XCore。All可以作为All目标的简写,并且名称区分大小写。若要只启用PowerPC和SystemZ目标,必须指定-DLLVM\underline{~}TARGETS\underline{~}To\underline{~}BUILD="PowerPC;SystemZ"。 65 | 66 | \item LLVM\underline{~}ENABLE\underline{~}PROJECTS:这是一个要构建的项目列表,由分号分隔。项目的源代码必须与llvm目录在同一级别(并排布局)。当前列表是clang, clangtools-extra, compiler-rt, debuginfo-tests, lib, libclc, libcxx, libcxxabi, libunwind, lld, lldb, llgo, mlir, openmp, parallel-libs, polly和pstl。All可以作为此列表中的所有项目的简写。要和LLVM一起构建Clang和llgo,必须指定-DLLVM\underline{~}ENABLE\underline{~}PROJECT="Clang;llgo"。 67 | 68 | \item LLVM\underline{~}ENABLE\underline{~}ASSERTIONS:如果设置为ON,则启用断言检查。这些检查有助于发现错误,在开发过程中非常有用。对于DEBUG版本,默认值为ON,否则为OFF。要打开断言检查(对于RELEASE版本),必须指定-DLLVM\underline{~}ENABLE\underline{~}ASSERTIONS=ON。 69 | 70 | \item LLVM\underline{~}ENABLE\underline{~}EXPENSIVE\underline{~}CHECKS:这启用了一些检查,会降低编译速度或消耗大量内存,默认值为OFF。要打开这些检查,必须设置-DLLVM\underline{~}ENABLE\underline{~}EXPENSIVE\underline{~}CHECKS=ON。 71 | 72 | \item LLVM\underline{~}APPEND\underline{~}VC\underline{~}REV: llc等LLVM工具显示它们所基于的LLVM版本(如果提供了version命令行选项)。此版本信息基于LLVM\underline{~}REVISION C宏。默认情况下,不仅LLVM版本,最新提交的Git哈希值也是版本信息的一部分。如果您正在跟踪主分支的开发,这将非常方便,因为它清楚地表明了该工具是基于哪个Git提交的。如果不是必需的,可以使用-DLLVM\underline{~}APPEND\underline{~}VC\underline{~}REV=OFF关闭。 73 | 74 | \item LLVM\underline{~}ENABLE\underline{~}THREADS:如果检测到线程库(通常是pthreads库),LLVM会自动包含线程支持。本例中,LLVM假定编译器支持\textbf{线程本地存储(TLS)}。如果不想要线程支持或者你的编译器不支持TLS,那么可以使用-DLLVM\underline{~}ENABLE\underline{~}THREADS=OFF来关闭它。 75 | 76 | \item LLVM\underline{~}ENABLE\underline{~}EH:LLVM项目不使用C++异常处理,所以默认关闭异常支持。此设置可能与您的项目正在链接的其他库不兼容。如果需要,可以通过指定-DLLVM\underline{~}ENABLE\underline{~}EH=ON来启用异常支持。 77 | 78 | \item LLVM\underline{~}ENABLE\underline{~}RTTI:LLVM使用一个轻量级的、自构建的系统来提供运行时类型信息。默认情况下,C++ RTTI的生成是关闭的。与异常处理支持一样,这可能与其他库不兼容。要开启C++ RTTI的生成,必须设置-DLLVM\underline{~}ENABLE\underline{~}RTTI=ON。 79 | 80 | \item LLVM\underline{~}ENABLE\underline{~}WARNINGS:如果可能的话,编译LLVM应该不会产生任何警告消息。默认情况下,打印警告消息的选项是打开的。要关闭它,必须设置-DLLVM\underline{~}ENABLE\underline{~}WARNINGS\allowbreak =OFF。 81 | 82 | \item LLVM\underline{~}ENABLE\underline{~}PEDANTIC:LLVM源文件应该符合C/C++标准。因此,默认情况下启用了对源的学究式检查。如果可能,也禁用编译器特定的扩展。要关闭此设置,必须指定-DLLVM\underline{~}\allowbreak ENABLE\underline{~}PEDANTIC=OFF。 83 | 84 | \item LLVM\underline{~}ENABLE\underline{~}WERROR:如果设置为ON,则所有警告都视为错误——发现警告,编译就会中止。它有助于在源代码中找到所有剩余的警告。默认情况下,是关闭的。要打开它,必须指定-DLLVM\underline{~}ENABLE\underline{~}WERROR=ON。 85 | 86 | \item LLVM\underline{~}OPTIMIZED\underline{~}TABLEGEN:通常,tablegen工具与LLVM的其他部分使用相同的选项构建。同时,tablegen用于生成大部分代码生成器。因此,tablegen在调试构建中要慢得多,从而显著增加了编译时间。如果将此选项设置为ON,则tablegen编译时会启用优化,即使是在调试构建中,也可能会减少编译时间,默认为OFF。要打开此选项,必须指定-DLLVM\underline{~}OPTIMIZED\underline{~}TABLEGEN=ON。 87 | 88 | \item LLVM\underline{~}USE\underline{~}SPLIT\underline{~}DWARF:如果构建编译器是gcc或Clang,那么打开这个选项编译器将在单独的文件中生成DWARF调试信息。减小的对象文件大小大大减少了调试构建的链接时间,默认为OFF。要开启此功能,必须指定-LLVM\underline{~}USE\underline{~}SPLIT\underline{~}DWARF=ON。 89 | 90 | \end{itemize} 91 | 92 | LLVM定义了更多的CMake变量。可以在CMake的LLVM文档中找到完整的列表 (\url{https://releases.llvm.org/12.0.0/docs/CMake.html\#llvm-specific-variables}),前面的列表只包含常用的一些。\par 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /content/1/chapter1/4.tex: -------------------------------------------------------------------------------- 1 | 本章中,您准备了开发机器来编译LLVM,克隆了LLVM GitHub代码库,并编译了LLVM和Clang。构建过程可以使用CMake变量进行定制。还学习了有用的变量以及如何更改它们。有了这些知识,您就可以根据需要调整LLVM的构建。\par 2 | 3 | 下一章中,我们将更详细地研究LLVM代码库的内容。将了解其中包含哪些项目以及这些项目是如何构建的。然后,将使用LLVM库创建自己的项目。最后,您将学习如何为不同的CPU架构编译LLVM。\par 4 | 5 | \newpage -------------------------------------------------------------------------------- /content/1/chapter2/0.tex: -------------------------------------------------------------------------------- 1 | 2 | LLVM代码库包含llvm-project根目录下的所有项目。为了有效地使用LLVM,知道哪些是可用的,以及在哪里可以找到。本章中,您将学习到以下内容:\par 3 | 4 | \begin{itemize} 5 | \item LLVM代码库的内容,包括最重要的顶层项目 6 | \item LLVM项目的结构,所有项目都统一的源码目录结构 7 | \item 如何使用LLVM库创建自己的项目,在自己的项目中使用LLVM的方法 8 | \item 如何针对不同的CPU体系结构,进行交叉编译 9 | \end{itemize} 10 | 11 | -------------------------------------------------------------------------------- /content/1/chapter2/1.tex: -------------------------------------------------------------------------------- 1 | 本章的代码文件可在\url{https://github.com/PacktPublishing/Learn-LLVM-12/tree/master/Chapter02/tinylang}获取。\par 2 | 3 | 你可以在视频中找到代码\url{https://bit.ly/3nllhED}。\par -------------------------------------------------------------------------------- /content/1/chapter2/2.tex: -------------------------------------------------------------------------------- 1 | 在第1章中,您克隆了LLVM库。这个库包含所有LLVM顶层项目,可以分为以下几类:\par 2 | 3 | \begin{itemize} 4 | \item LLVM核心库和附加内容 5 | \item 编译器和工具 6 | \item 运行时库 7 | \end{itemize} 8 | 9 | 下一节中,我们将进一步研究这些。\par 10 | 11 | \hspace*{\fill} \par %插入空行 12 | \textbf{LLVM核心库和附加内容} 13 | 14 | LLVM核心库位于llvm目录中。为主流的CPU提供了一组带有优化器和代码生成的库,还提供基于这些库的工具。LLVM静态编译器llc将LLVM\textbf{中间表示(IR)}编写的文件作为输入,并将其编译为位码、汇编器输出或二进制对象文件。像llvm-objdump和llvm-dwarfdump这样的工具允许检查目标文件,而像llvm-ar这样的工具允许从一组目标文件创建静态库,还包括帮助开发LLVM本身的工具,例如:bugpoint工具可以帮助找到LLVM中崩溃的最小测试用例。llvm-mc可以对机器代码进行操作:该工具可以对机器指令进行汇编和反汇编,这对添加新的指令很有帮助。\par 15 | 16 | LLVM核心库由C++编写的。此外,还提供了C接口和Go、Ocaml和Python接口。\par 17 | 18 | Polly项目位于polly目录中,向LLVM添加了另一组优化。它基于一种叫做\textbf{多面体模型}的数学表示,使用这种方法,可以进行复杂的优化,如使用缓存局部优化的循环。MLIR项目旨在为LLVM提供多级中间表示。\par 19 | 20 | \textbf{MLIR}旨在为LLVM提供\textbf{多级的间表示}。LLVM IR已经属于底层,并包括源语言的某些信息(这些信息在编译器生成IR时丢失了)。MLIR使LLVM IR具有可扩展性,并在特定领域可以捕获该信息,可以在mlir目录中找到相应的源码。\par 21 | 22 | \hspace*{\fill} \par %插入空行 23 | \textbf{编译器和工具} 24 | 25 | 名为Clang(\url{http://clang.llvm.org/})的C/C++/Objective-C/Object-C++编译器是LLVM项目的一部分,源码位于clang目录中。它提供了一组库,用于从C、C++、Objective-C和Objective-C++源码进行词法分析、解析、语义分析和生成LLVM IR。Clang是基于这些库的编译器驱动程序。另一个工具是clang-format,可以根据用户提供的规则格式化C/C++源码。\par 26 | 27 | Clang的目标是兼容GCC(GNU C/C++编译器)和CL(Microsoft C/C++编译器)。\par 28 | 29 | C/C++的其他工具由同名目录下的clang-tools-extra项目提供。值得注意的是clang-tidy,它是C/C++的Lint样式检查器。clang-tidy使用clang库来解析源代码,并使用静态分析检查源代码。与编译器相比,工具可以捕获更多的潜在错误,但会增加运行时间。\par 30 | 31 | Llgo是一个用于Go编程语言的编译器,位于Llgo目录下。用Go编写的,并使用LLVM核心库的Go绑定LLVM接口。Llgo的目标是与参考编译器(\url{https://golang.org/})兼容,但目前支持的架构是64位x86 Linux。该项目似乎没有继续进行维护,并可能在未来删除。\par 32 | 33 | 编译器创建的对象文件必须与运行时库链接在一起,以形成可执行文件。这是lld(\url{http://lld.llvm.org/})的任务,LLVM链接器位于lld目录中。连接器支持ELF、COFF、Mach-O和WebAssembly格式。\par 34 | 35 | 没有调试器的编译器工具集是不完整的!LLVM调试器名为lldb(\url{http://lldb.llvm.org/}),位于同名的目录中。该接口类似于GDB、GNU调试器,并且该工具支持C、C++和Objective-C。调试器可以扩展,因此可以添加对其他编程语言的支持。\par 36 | 37 | \hspace*{\fill} \par %插入空行 38 | \textbf{运行时库} 39 | 40 | 除了编译器,运行时库还需要编程语言支持。所有项目都位于同一个目录中:\par 41 | 42 | \begin{itemize} 43 | \item compiler-rt项目提供了独立于编程语言的支持库。它包括泛型函数,例如:可在32位(i386)机上使用的64位除法、各种sanitizer、模糊库和分析库。 44 | 45 | \item libunwind库提供了基于DWARF标准的堆栈展开帮助函数。这通常用于C++等语言的异常处理。该库用C编写,函数没有绑定到特定的异常处理模型上。 46 | 47 | \item libcxxabi库在libunwind上实现了C++的异常处理,并为其提供了标准的C++函数。 48 | 49 | \item libcxx是C++标准库的实现,包括iostreams和STL。另外,pstl项目提供了并行版本的STL算法。 50 | 51 | \item libclc是OpenCL的运行时库。OpenCL是异构并行计算的标准,有助于将计算任务转移到GPU上。 52 | 53 | \item libc旨在提供一个完整的C库。这个项目仍处于早期阶段。 54 | 55 | \item OpenMP项目提供对OpenMP API的支持。OpenMP可以帮助多线程编程,例如:可以基于源代码中的注释并行化循环。 56 | \end{itemize} 57 | 58 | 尽管这是一个很长的项目列表,但所有项目的结构都是相似的。我们将在下一节中查看统一的目录结构。\par 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /content/1/chapter2/3.tex: -------------------------------------------------------------------------------- 1 | 所有LLVM项目都有统一的目录结构。让我们比较一下LLVM和GCC,即GNU编译器集合。几十年来,GCC几乎为您能想到的每一个系统都提供了成熟的编译器。但除了编译器,没有任何工具可以用这些代码,原因是它不为重用而设计,而LLVM截然不同。\par 2 | 3 | LLVM的每个功能都有明确的API定义,并放在自己的库中。Clang项目有一个库,可以将C/C++源文件写入令牌流。解析器库将该令牌流转换为抽象语法树(由库支持)。语义分析、代码生成,甚至编译器驱动程序都作为库提供。众所周知的Clang工具,只是一个链接到这些库的应用程序。\par 4 | 5 | 这样做的好处很明显:想要构建一个需要C++文件抽象语法树(AST)的工具时,可以重用这些库的功能来构建AST。不需要语义分析和生成代码,也不需要链接这些库。所有LLVM项目都遵循这个原则,包括核心库!\par 6 | 7 | 每个项目都有类似的结构。因为CMake用于生成构建文件,所以每个项目都用CMakeLists.txt来描述项目的构建。如果需要额外的CMake模块或支持文件,可以将它们存储在cmake子目录中,而现成的模块则放在cmake/modules中。\par 8 | 9 | 库和工具大多是用C++编写的。源文件放在lib目录下,头文件放在include目录下。因为一个项目通常由几个库组成,所以lib目录中有每个库的目录。如果有必要,还会套娃,例如:在llvm/lib目录中有Target目录,该目录包含特定于目标的更加底层的操作。除了一些源文件外,每个目标还有子目录,这些子目录会再次编译成库。每个目录都有一个CMakeLists.txt文件,该文件描述了如何构建库以及哪些子目录还包含源代码。\par 10 | 11 | include目录有级别。为了使包含文件的名称唯一,路径名包含项目名称,并且是include下的第一个子目录。只有在这个文件夹中,lib目录的结构才会重复。\par 12 | 13 | 应用程序的源码位于tools和utils目录中,utils目录中是在编译或测试期间使用的内部应用程序。它们通常不是用户安装的一部分,tools目录包含用于最终用户的应用程序。这两个目录中,每个应用程序都有自己的子目录。与lib目录一样,每个包含source的子目录都有CMakeLists.txt。\par 14 | 15 | 编译器必须正确的生成代码,这需要通过测试套件来实现。unittest目录包含使用Google Test框架的单元测试。这主要用于单个函数和无法通过其他方式测试的独立功能。test目录中是LIT测试,这些测试使用llvm-lit实用程序执行测试。llvm-lit扫描文件中的shell命令并执行它们。该文件包含用作测试输入的源代码,例如:LLVM IR。文件中嵌入编译命令,由llvm-lit执行。然后,在FileCheck工具的帮助下,验证该步骤的输出。这个程序从文件中读取检查语句,并将它们与另一个文件进行匹配。LIT测试本身位于test目录下的子目录中,对于lib的目录结构的遵循不是很严格。\par 16 | 17 | 文档(通常是reStructuredText)放在docs目录中。如果项目提供了示例,则位于examples目录中。\par 18 | 19 | 根据项目的需要,还可以有其他目录。值得注意的是,提供运行时库的项目将源代码放在src目录中,并使用lib目录作为库导出定义。compiler-rt和libclc项目包含与体系结构相关的代码,它总是放在以目标体系结构命名的子目录中(例如,i386或ptx)。\par 20 | 21 | 总之,提供样例库并带有驱动工具项目的总体结构如下所示:\par 22 | 23 | \hspace*{\fill} \par %插入空行 24 | \begin{center} 25 | \includegraphics{content/1/chapter2/images/1.jpg}\\ 26 | 图2.1 – 项目的目录结构 27 | \end{center} 28 | 29 | 我们自己的项目也将遵循这个结构。\par 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /content/1/chapter2/5.tex: -------------------------------------------------------------------------------- 1 | 2 | 今天,许多小型计算机(如树莓派)在使用,只有有限的资源。在这样的计算机上运行编译器通常是不可能的,或者需要太多的运行时间。因此,编译器的一个常见需求是为不同的CPU体系结构生成代码,并创建可执行文件的整个过程称为\textbf{交叉编译}。前一节中,创建了一个基于LLVM库的小型示例应用。现在,我们将使用这个程序,并为不同的目标编译它。\par 3 | 4 | 使用交叉编译,涉及到两个系统:编译器在主机系统上运行,并为目标系统生成代码。为了表示这些系统,我们使用了所谓的“三元组表达式”。这是一个配置字符串,通常由CPU架构、供应商和操作系统组成。通常还会添加更多关于环境的信息。例如,x86\underline{~}64-pc-win32用于运行在64位x86 CPU上的Windows系统。CPU架构是x86\underline{~}64, pc是通用供应商,win32是操作系统。各部分用连字符连接。在ARMv8 CPU上运行的Linux系统使用aarch64-unknown-linux-gnu作为三元组表达式。aarch64是CPU架构。操作系统是linux,运行gnu环境。没有基于linux的系统供应商,所以这一部分是未知的。对于特定目的而言,那些不为人所知或不重要的部分通常会被省略,则aarch64-linux-gnu描述了相同的Linux系统。\par 5 | 6 | 假设您的开发机器在x86 64位CPU上运行Linux,并且您希望交叉编译到一个运行Linux的ARMv8 CPU系统。主机表示为x86\underline{~}64-linux-gnu,目标三元组表达式是aarch64-linux-gnu。不同的系统有不同的特点,应用程序必须以可移植的方式编写,否则失败的原因会很难查找。常见的陷阱如下:\par 7 | 8 | \begin{itemize} 9 | \item \textbf{字节顺序}:多字节值存储在内存中的顺序可以不同。 10 | \item \textbf{指针大小}:指针的大小随CPU架构的不同而不同(通常为16位、32位或64位)。C类型int可能不够大,无法保存指针。 11 | \item \textbf{类型差异}:数据类型通常与硬件密切相关。long double类型可以使用64位(ARM)、80位(X86)或128位(ARMv8)。PowerPC系统可以对长双精度使用双精度算法,它通过使用两个64位双精度值的组合来提供更高的精度。 12 | \end{itemize} 13 | 14 | 如果不注意这些要点,那么即使应用在您的主机系统上完美地运行,也可能在目标平台上表现惊人,或者会崩溃。LLVM库在不同的平台上进行了测试,并包含了针对上述问题的可移植解决方案。\par 15 | 16 | 交叉编译时,需要使用以下工具:\par 17 | 18 | \begin{itemize} 19 | \item 为目标生成代码的编译器 20 | \item 可生成二进制文件的链接器 21 | \item 目标的头文件和库 22 | \end{itemize} 23 | 24 | Ubuntu和Debian发行版都有支持交叉编译的软件包。下面的设置中,我们将利用这一点。gcc和g++编译器、ld链接器和库都可以作为生成ARMv8代码和可执行文件的预编译可执行文件使用。输入以下命令:\par 25 | 26 | \begin{tcolorbox}[colback=white,colframe=black] 27 | \$ sudo apt install gcc-8-aarch64-linux-gnu $\setminus$ \\ 28 | \hspace*{1cm}g++-8-aarch64-linux-gnu binutils-aarch64-linux-gnu $\setminus$ \\ 29 | \hspace*{1cm}libstdc++-8-dev-arm64-cross 30 | \end{tcolorbox} 31 | 32 | 新文件安装在/usr/aarch64-linux-gnu目录下。directory目标系统的(逻辑)根目录,它包含通常的bin、lib和include目录,并且交叉编译器(aarch64-linux-gnu-gcc-8和aarch64-linux-gnu-g++-8)知道这个目录。\par 33 | 34 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black, title=在其他系统上交叉编译] 35 | 如果您的发行版没有附带所需的工具链,那么可以从源代码构建。必须配置gcc和g++编译器来生成目标系统的代码,binutils工具需要处理目标系统的文件。此外,C和C++库需要用这个工具链来编译。该步骤因所使用的操作系统以及主机和目标体系结构而异。可以通过网页上,搜索gcc cross-compile ,从而找到找到相应的指令。 36 | \end{tcolorbox} 37 | 38 | 通过这些准备,已经准备好交叉编译示例应用程序(包括LLVM库)了,除了一个小细节外。LLVM在构建过程中使用tablegen工具。交叉编译期间,为目标体系结构编译所有内容,包括此工具。可以在第1章“安装LLVM”中使用llvm-tblgen,也可以只编译这个工具。假设从GitHub克隆代码目录下,键入如下命令:\par 39 | 40 | \begin{tcolorbox}[colback=white,colframe=black] 41 | \$ mkdir build-host \\ 42 | \$ cd build-host \\ 43 | \$ cmake -G Ninja $\setminus$ \\ 44 | \hspace*{1cm}-DLLVM\underline{~}TARGETS\underline{~}TO\underline{~}BUILD="X86" $\setminus$ \\ 45 | \hspace*{1cm}-DLLVM\underline{~}ENABLE\underline{~}ASSERTIONS=ON $\setminus$ \\ 46 | \hspace*{1cm}-DCMAKE\underline{~}BUILD\underline{~}TYPE=Release $\setminus$ \\ 47 | \hspace*{1cm}../llvm-project/llvm \\ 48 | \$ ninja llvm-tblgen \\ 49 | \$ cd .. 50 | \end{tcolorbox} 51 | 52 | 这些步骤现在应该很熟悉了。创建并输入构建目录。CMake命令仅为X86目标创建LLVM构建文件。为了节省空间和时间,已经完成了发布构建,但支持断言以捕获可能的错误。只有llvmtblgen工具是用Ninja编译的。\par 53 | 54 | 有了llvm-tblgen工具,就可以开始交叉编译了。CMake命令行非常长,因此可能需要将该命令存储在脚本文件中。与之前的版本不同的是,需要提供更多的信息:\par 55 | 56 | \begin{tcolorbox}[colback=white,colframe=black] 57 | \$ mkdir build-target \\ 58 | \$ cd build-target \\ 59 | \$ cmake -G Ninja $\setminus$ \\ 60 | \hspace*{1cm}-DCMAKE\underline{~}CROSSCOMPILING=True $\setminus$ \\ 61 | \hspace*{1cm}-DLLVM\underline{~}TABLEGEN=../build-host/bin/llvm-tblgen $\setminus$ \\ 62 | \hspace*{1cm}-DLLVM\underline{~}DEFAULT\underline{~}TARGET\underline{~}TRIPLE=aarch64-linux-gnu $\setminus$ \\ 63 | \hspace*{1cm}-DLLVM\underline{~}TARGET\underline{~}ARCH=AArch64 $\setminus$ \\ 64 | \hspace*{1cm}-DLLVM\underline{~}TARGETS\underline{~}TO\underline{~}BUILD=AArch64 $\setminus$ \\ 65 | \hspace*{1cm}-DLLVM\underline{~}ENABLE\underline{~}ASSERTIONS=ON $\setminus$ \\ 66 | \hspace*{1cm}-DLLVM\underline{~}EXTERNAL\underline{~}PROJECTS=tinylang $\setminus$ \\ 67 | \hspace*{1cm}-DLLVM\underline{~}EXTERNAL\underline{~}TINYLANG\underline{~}SOURCE\underline{~}DIR=../tinylang $\setminus$ \\ 68 | \hspace*{1cm}-DCMAKE\underline{~}INSTALL\underline{~}PREFIX=../target-tinylang $\setminus$ \\ \hspace*{1cm}-DCMAKE\underline{~}BUILD\underline{~}TYPE=Release $\setminus$ \\ 69 | \hspace*{1cm}-DCMAKE\underline{~}C\underline{~}COMPILER=aarch64-linux-gnu-gcc-8 $\setminus$ \\ 70 | \hspace*{1cm}-DCMAKE\underline{~}CXX\underline{~}COMPILER=aarch64-linux-gnu-g++-8 $\setminus$ \\ 71 | \hspace*{1cm}../llvm-project/llvm \\ 72 | \$ ninja 73 | \end{tcolorbox} 74 | 75 | 同样,创建构建目录并进入。一些CMake参数之前没有使用过,需要解释一下:\par 76 | 77 | \begin{itemize} 78 | \item CMAKE\underline{~}CROSSCOMPILING设置为ON,则告诉CMake我们正在交叉编译。 79 | \item LLVM\underline{~}TABLEGEN指定llvm-tblgen工具要使用的路径。这来自上一个构建。 80 | \item LLVM\underline{~}DEFAULT\underline{~}TARGET\underline{~}TRIPLE是目标架构的三元组表达式。 81 | \item LLVM\underline{~}TARGET\underline{~}ARCH使用即时(JIT)代码生成,默认为主机的架构。对于交叉编译,必须将其设置为目标架构。 82 | \item LLVM\underline{~}TARGETS\underline{~}TO\underline{~}BUILD是一个目标列表,LLVM应该包含这些目标的代码生成器。该列表至少应该包括目标体系结构。 83 | \item CMAKE\underline{~}C\underline{~}COMPILER和CMAKE\underline{~}CXX\underline{~}COMPILER指定编译所用的C和C++编译器。交叉编译器的二进制文件是用目标三元表达式,CMake无法自动找到。 84 | \end{itemize} 85 | 86 | 使用其他参数,发布版本将请求启用断言进行构建,我们的tinylang应用程序将作为LLVM的一部分构建(如上一节所示)。编译过程完成后,可以使用file命令检查是否真的为ARMv8创建了一个二进制文件。运行\$ file bin/tinylang,检查输出是否显示它是ARM aarch64架构的ELF 64位对象。\par 87 | 88 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black, title=使用clang进行交叉编译] 89 | 由于LLVM为不同的体系结构生成代码,显然可以使用clang进行交叉编译。这里的障碍是,LLVM不提供所有所需的组件,例如:C库缺失。因此,必须混合使用LLVM和GNU工具,因此需要告诉CMake更多关于正在使用的环境的信息。\verb|--|target=(为不同的目标启用代码生成),\verb|--|sysroot=(目标的根目录的路径,参见前面)、-I(搜索头文件的路径)和-L(搜索库的路径)。在CMake运行期间,会编译一个小的应用程序。如果设置有问题,CMake会报错。这个步骤足以检查您是否有一个正确的工作环境。常见的问题包括选择错误的头文件,由于不同的库名导致的链接失败,以及错误的搜索路径。 90 | \end{tcolorbox} 91 | 92 | 交叉编译非常复杂。有了本节的说明,您将能够针对选择的目标架构交叉编译相应的应用程序。\par 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /content/1/chapter2/6.tex: -------------------------------------------------------------------------------- 1 | 本章中,您了解了作为LLVM存储库一部分的项目以及常用的布局。为自己的小型应用程序复制了这种结构,为构建更复杂的应用程序奠定了基础。作为编译器构造的最高原则,您还学习了如何为另一个目标体系结构交叉编译应用程序。\par 2 | 3 | 在下一章中,我们将概述示例语言tinylang。您将了解编译器必须执行的任务,以及LLVM库支持哪些功能。\par 4 | 5 | \newpage -------------------------------------------------------------------------------- /content/1/chapter2/images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/1/chapter2/images/1.jpg -------------------------------------------------------------------------------- /content/1/chapter2/images/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/1/chapter2/images/2.jpg -------------------------------------------------------------------------------- /content/1/chapter2/images/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/1/chapter2/images/3.jpg -------------------------------------------------------------------------------- /content/1/chapter3/0.tex: -------------------------------------------------------------------------------- 1 | 编译技术是计算机科学的一个重要领域,其的高级任务是将源语言翻译成机器代码。通常,这个任务分为两部分:前端和后端。前端主要处理源语言,而后端负责生成机器代码。\par 2 | 3 | 本章中,我们将讨论以下主题:\par 4 | 5 | \begin{itemize} 6 | \item 编译器的构建块,您将了解编译器中的常用组件。 7 | \item 算术表达式语言,将向您介绍一种示例语言。您将学习如何使用语法来定义一种语言。 8 | \item 词法分析,将讨论如何实现语言的词法分析器。 9 | \item 语法分析,包括如何构造语法解析器。 10 | \item 语义分析,将了解如何实现语义检查。 11 | \item LLVM后端代码生成,将讨论如何与LLVM后端接口,以及如何将所有阶段聚合在一起,来创建一个完整的编译器。 12 | \end{itemize} 13 | -------------------------------------------------------------------------------- /content/1/chapter3/1.tex: -------------------------------------------------------------------------------- 1 | 本章的代码文件可在 \url{https://github.com/PacktPublishing/Learn-LLVM-12/tree/master/Chapter03/calc}获取。\par 2 | 3 | 你可以在视频中找到代码\url{https://bit.ly/3nllhED}。\par -------------------------------------------------------------------------------- /content/1/chapter3/2.tex: -------------------------------------------------------------------------------- 1 | 上世纪中叶计算机问世后,很快,一种比汇编语言更抽象的语言在编程方面就异军突起了。早在1957年,Fortran作为第一种可用的高级程序设计语言问世。从那时起,成千上万种编程语言被开发出来。事实证明,所有的编译器都必须解决相同的任务,编译器的实现应该根据这些任务进行架构和设计。\par 2 | 3 | 抽象的来看,编译器由两部分组成:前端和后端。前端负责特定于语言的任务,读取源文件并计算语义分析表示,通常是带注释的抽象语法树(AST)。后端从前端的结果创建优化的机器码。区分前端和后端的动机是可重用性。假设前端和后端之间的接口定义得很好,就可以将一个C和一个Modula-2前端连接到同一个后端。或者,当有一个x86后端和一个Sparc后端,那么可以将您的C++前端与二者相连。\par 4 | 5 | 前端和后端有特定的结构。前端通常执行以下任务:\par 6 | 7 | \begin{enumerate} 8 | \item 词法分析器(Lexical analyzer,简称Lexer)读取源文件并生成一个令牌流。 9 | \item 解析器从令牌流创建一个AST。 10 | \item 语义分析器向AST添加语义信息。 11 | \item 代码生成器从AST生成一个中间表示(IR)。 12 | \end{enumerate} 13 | 14 | 中间表示是后端接口。后端执行以下任务:\par 15 | 16 | \begin{enumerate} 17 | \item 后端在IR上执行与目标无关的优化。 18 | \item 然后为IR代码选择指令。 19 | \item 之后,对指令执行与目标相关的优化。 20 | \item 最后,产生汇编程序代码或目标文件。 21 | \end{enumerate} 22 | 23 | 当然,这些指令只是概念上的,实现会有很大的不同。LLVM核心库将中间表示定义为后端标准接口,其他工具可以使用带注释的AST,并且C的预处理器是专用于C的。其可以实现为输出预处理C源的应用程序,也可以实现为词法分析器和解析器之间的中间件。某些情况下,AST不能显式构造。如果要实现的语言不是太复杂,可以组合解析器和语义分析器。然后在解析时生成代码,即使给定的编程语言实现没有显式地命名这些组件。不过,以上的任务是必须要完成的。\par 24 | 25 | 在下面的小节中,我们将为表达式语言构造一个编译器,该语言从其输入生成LLVM IR。LLVM静态编译器llc表示后端,可以使用它将IR编译成目标代码。这一切都要从定义一种语言开始。\par 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /content/1/chapter3/3.tex: -------------------------------------------------------------------------------- 1 | 算术表达式是每一种编程语言的一部分。下面是一个叫做calc的算术表达式计算语言的例子。calc表达式会编译到一个应用程序中,计算以下表达式:\par 2 | 3 | \begin{tcolorbox}[colback=white,colframe=black] 4 | with a, b: a * (4 + b) 5 | \end{tcolorbox} 6 | 7 | 表达式中使用的变量必须用with关键字声明。这个程序可以编译成应用程序,它向用户询问a和b变量的值并打印结果。\par 8 | 9 | 简单的示例总是受欢迎的,但作为编译器作者,需要比这更全面的规范来实现和测试。编程语言语法的载体是语法。\par 10 | 11 | \hspace*{\fill} \par %插入空行 12 | \textbf{指定编程语言语法的形式} 13 | 14 | 语言的元素,如:关键字、标识符、字符串、数字和操作符,称为标记。其实,程序是一个标记序列,语法指定哪些序列是有效的。\par 15 | 16 | 通常,语法是用扩展的巴科斯-诺尔范式(EBNF)编写的。其中一个语法规则是有一个左手边和一个右手边。左边只是一个叫做非终结符的符号,右手边包括用于替代和重复的非终结符、标记和元符号。来看看calc语言的语法:\par 17 | 18 | \begin{tcolorbox}[colback=white,colframe=black] 19 | calc : ("with" ident ("," ident)* ":")? expr ; \\ 20 | expr : term (( "+" | "-" ) term)* ; \\ 21 | term : factor (( "*" | "/") factor)* ; \\ 22 | factor : ident | number | "(" expr ")" ; \\ 23 | ident : ([a-zAZ])+ ; \\ 24 | number : ([0-9])+ ; 25 | \end{tcolorbox} 26 | 27 | 第一行中,calc是非终止符。如果没有特别说明,则语法的第一个非结束符是开始符号。冒号,:,是规则左边和右边之间的分隔符。",", "和":"是表示该字符串的标记。括号用于分组,组是可选的,也可以重复。右括号后的问号?表示可选组。星号*表示零次或多次重复,而加号+表示一次或多次重复。ident和expr是非终止符。每一个,都对应另一个规则。分号;表示规则的结束。第二行中的管道|表示一个替代方案。最后,最后两行中的括号[]表示字符类。有效字符写在括号内,例如:[a-zA-Z]字符类匹配一个大写字母或小写字母,([a-zA-Z])+匹配这些字母中的一个或多个。这相当于正则表达式。\par 28 | 29 | \hspace*{\fill} \par %插入空行 30 | \textbf{语法对于编译器作者的帮助} 31 | 32 | 首先,定义了所有标记,这是创建词法分析器所需要的,语法规则可以翻译成解析器。当然,如果出现关于解析器是否正确工作的问题,那么语法可以作为一个很好的规范。\par 33 | 34 | 然而,语法并不是定义编程语言的所有方面,语法的意义(语义)也必须定义。为此目的也开发了语法形式,类似于最初引入该语言,通常在纯文本中指定。\par 35 | 36 | 掌握了这些知识后,接下来的两节将展示词法分析如何将输入转换为标记序列,以及如何用C++编写语法,从而进行语法分析。\par 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /content/1/chapter3/5.tex: -------------------------------------------------------------------------------- 1 | 语义分析器遍历AST并检查语言的各种语义规则,例如:在使用变量之前必须声明变量,或者变量的类型必须在表达式中兼容。如果发现可以改进的情况,还可以输出警告。对于表达式语言,语义分析器必须检查每个使用的变量是否声明,这是语言所需。一个可能的扩展(这里不实现)是在未使用声明的变量时输出警告消息。\par 2 | 3 | 语义分析器在Sema类中实现,语义分析由semantic()执行。下面是完整的Sema.h头文件:\par 4 | 5 | \begin{lstlisting}[caption={}] 6 | #ifndef SEMA_H 7 | #define SEMA_H 8 | 9 | #include "AST.h" 10 | #include "Lexer.h" 11 | 12 | class Sema { 13 | public: 14 | bool semantic(AST *Tree); 15 | }; 16 | #endif 17 | \end{lstlisting} 18 | 19 | 实现在Sema.cpp文件中。有趣的部分是语义分析,并使用访问者实现。其基本思想是,每个声明变量的名称存储在一个集合中。创建时,可以检查每个命名是否唯一,然后检查命名是否在集合中: 20 | 21 | \begin{lstlisting}[caption={}] 22 | #include "Sema.h" 23 | #include "llvm/ADT/StringSet.h" 24 | namespace { 25 | class DeclCheck : public ASTVisitor { 26 | llvm::StringSet<> Scope; 27 | bool HasError; 28 | 29 | enum ErrorType { Twice, Not }; 30 | void error(ErrorType ET, llvm::StringRef V) { 31 | llvm::errs() << "Variable " << V << " " 32 | << (ET == Twice ? "already" : "not") 33 | << " declared\n"; 34 | HasError = true; 35 | } 36 | public: 37 | DeclCheck() : HasError(false) {} 38 | 39 | bool hasError() { return HasError; } 40 | \end{lstlisting} 41 | 42 | 与Parser类中一样,使用标记来指示发生了错误,这些名字存储在Scope的集合中。在持有一个变量名的Factor节点中,我们检查该变量名是否在这个集合中。 43 | 44 | \begin{lstlisting}[caption={}] 45 | virtual void visit(Factor &Node) override { 46 | if (Node.getKind() == Factor::Ident) { 47 | if (Scope.find(Node.getVal()) == Scope.end()) 48 | error(Not, Node.getVal()); 49 | } 50 | }; 51 | \end{lstlisting} 52 | 53 | 对于BinaryOp节点,只需要检查两边是否存在,并且是否已经访问过: 54 | 55 | \begin{lstlisting}[caption={}] 56 | virtual void visit(BinaryOp &Node) override { 57 | if (Node.getLeft()) 58 | Node.getLeft()->accept(*this); 59 | else 60 | HasError = true; 61 | if (Node.getRight()) 62 | Node.getRight()->accept(*this); 63 | else 64 | HasError = true; 65 | }; 66 | \end{lstlisting} 67 | 68 | 在WithDecl节点中,填充集合并开始遍历表达式: 69 | 70 | \begin{lstlisting}[caption={}] 71 | virtual void visit(WithDecl &Node) override { 72 | for (auto I = Node.begin(), E = Node.end(); I != E; 73 | ++I) { 74 | if (!Scope.insert(*I).second) 75 | error(Twice, *I); 76 | } 77 | if (Node.getExpr()) 78 | Node.getExpr()->accept(*this); 79 | else 80 | HasError = true; 81 | }; 82 | }; 83 | } 84 | \end{lstlisting} 85 | 86 | semantic()方法会遍历树,并返回错误标志: 87 | 88 | \begin{lstlisting}[caption={}] 89 | bool Sema::semantic(AST *Tree) { 90 | if (!Tree) 91 | return false; 92 | DeclCheck Check; 93 | Tree->accept(Check); 94 | return Check.hasError(); 95 | } 96 | \end{lstlisting} 97 | 98 | 如果没有使用声明的变量,也可以打印警告消息。这算是给读者留的课后作业。如果语义分析没有出现错误,那么就可以使用AST生成LLVM IR。我们将在下一节中完成这项工作。\par 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /content/1/chapter3/7.tex: -------------------------------------------------------------------------------- 1 | 本章中,了解编译器的组件。算术表达式语言用来介绍编程语言的语法。然后,学习了如何为该语言开发前端的典型组件:词法分析器、解析器、语义分析器和代码生成器。代码生成器只生成LLVM IR,而LLVM静态编译器llc用于从它创建目标文件。最后,开发了第一个基于llvm的编译器!\par 2 | 3 | 下一章中,您将加深这方面的知识,以便为语言构建前端。\par 4 | 5 | \newpage -------------------------------------------------------------------------------- /content/2/Part-2.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 您将学习如何开发自己的编译器,我们将从构造前端开始,读取源文件并创建抽象语法树。然后,您将学习如何从源文件生成LLVM IR。使用LLVM的优化功能,您将创建优化的机器码。您还将了解更多高级主题,包括为面向对象语言构造生成LLVM IR,以及如何添加调试元数据。\par 4 | 5 | 本节包括以下几章:\par 6 | 7 | \begin{itemize} 8 | \item 第4章,将源码转换为抽象语法树 9 | \item 第5章,生成IR——基础知识 10 | \item 第6章,生成高级语言结构的IR 11 | \item 第7章,生成IR——进阶知识 12 | \item 第8章,优化IR 13 | \end{itemize} 14 | 15 | \newpage -------------------------------------------------------------------------------- /content/2/chapter4/0.tex: -------------------------------------------------------------------------------- 1 | 编译器通常分为两部分:前端和后端。本章中,我们将实现一个程序设计语言的前端,就是处理源语言的部分。我们将学习实际的编译器使用的技术,并应用到我们自己的编程语言中。\par 2 | 3 | 我们将从定义编程语言的语法开始,并以抽象语法树(AST)结束(将是代码生成的基础)。您可以将此方法用于任何想要为其实现编译器的编程语言。\par 4 | 5 | 本章中,您将学习以下主题:\par 6 | 7 | 8 | \begin{itemize} 9 | \item 定义一种编程语言——tinylang语言,它是编程语言的子集,必须为它实现一个编译器前端。 10 | \item 为编译器创建项目布局。 11 | \item 管理源文件和用户消息,这使您了解如何处理多个输入文件,以及如何以一种友善的方式告知用户问题所在。 12 | \item 构建词法分析器,讨论如何将词法分析器分解成模块部分。 13 | \item 构建递归下降解析器,讨论从语法派生解析器,以及执行语法分析时可以使用的规则。 14 | \item 使用bison和flex解析器和分析器,其中您将使用工具根据规范轻松地生成解析器和分析器。 15 | \item 执行语义分析,您将创建AST并评估其属性,这会与解析器有一些交集。 16 | \end{itemize} 17 | 18 | 有了本章学到的技能,您将能够为任何编程语言构建编译器前端。\par -------------------------------------------------------------------------------- /content/2/chapter4/1.tex: -------------------------------------------------------------------------------- 1 | 本章的代码文件可在\url{https://github.com/ 2 | PacktPublishing/Learn-LLVM-12/tree/master/Chapter04}获取。\par 3 | 4 | 你可以在视频中找到代码\url{https://bit.ly/3nllhED}。\par -------------------------------------------------------------------------------- /content/2/chapter4/2.tex: -------------------------------------------------------------------------------- 1 | 实际的编程语言比前一章简单的calc语言更复杂。为了更详细地研究它,我将在本章和接下来的章节中使用Modula-2的子集。Modula-2设计良好,可选地支持泛型和面向对象编程(OOP)(并不是说要在本书中创建一个完整的Modula-2编译器)。因此,我将把这个子集命名为tinylang。\par 2 | 3 | 让我们快速浏览一下tinylang语法的子集。在接下来的小节中,我们将从该语法派生词法分析器和解析器:\par 4 | 5 | \begin{tcolorbox}[colback=white,colframe=black] 6 | compilationUnit \\ 7 | \hspace*{0.5cm}: "MODULE" identifier ";" ( import )* block identifier "." ; \\ 8 | Import : ( "FROM" identifier )? "IMPORT" identList ";" ; \\ 9 | Block \\ 10 | \hspace*{0.5cm}: ( declaration )* ( "BEGIN" statementSequence )? "END" ; 11 | \end{tcolorbox} 12 | 13 | 在Modula-2中,编译单元以MODULE关键字开头,然后是模块的名称。模块的内容可以是导入的模块列表、声明和在初始化时运行的语句的块:\par 14 | 15 | \begin{tcolorbox}[colback=white,colframe=black] 16 | declaration \\ 17 | \hspace*{0.5cm}: "CONST" ( constantDeclaration ";" )* \\ 18 | \hspace*{0.5cm}| "VAR" ( variableDeclaration ";" )* \\ 19 | \hspace*{0.5cm}| procedureDeclaration ";" ; 20 | \end{tcolorbox} 21 | 22 | 声明引入常量、变量和过程。已声明的常量以CONST关键字作为前缀。类似地,变量声明以VAR关键字开头。声明常量非常简单:\par 23 | 24 | \begin{tcolorbox}[colback=white,colframe=black] 25 | constantDeclaration : identifier "=" expression ; 26 | \end{tcolorbox} 27 | 28 | 标识符是常量的名称。该值派生自表达式,该表达式必须在编译时是可计算的。声明变量有点复杂:\par 29 | 30 | \begin{tcolorbox}[colback=white,colframe=black] 31 | variableDeclaration : identList ":" qualident ; \\ 32 | qualident : identifier ( "." identifier )* ; \\ 33 | identList : identifier ( "," identifier)* ; 34 | \end{tcolorbox} 35 | 36 | 为了能够一次声明多个变量,必须使用标识符列表,类型的名称可能来自另一个模块。本例中以模块名作为前缀,称为限定标识符。而过程需要更多详细信息:\par 37 | 38 | \begin{tcolorbox}[colback=white,colframe=black] 39 | procedureDeclaration \\ 40 | \hspace*{0.5cm}: "PROCEDURE" identifier ( formalParameters )? ";" \\ 41 | \hspace*{1cm}block identifier ; \\ 42 | formalParameters \\ 43 | \hspace*{0.5cm}: "(" ( formalParameterList )? ")" ( ":" qualident )? ; \\ 44 | formalParameterList \\ 45 | \hspace*{0.5cm}: formalParameter (";" formalParameter )* ; \\ 46 | formalParameter : ( "VAR" )? identList ":" qualident ; 47 | \end{tcolorbox} 48 | 49 | 在前面的代码中,您可以看到常量、变量和过程是如何声明的。过程可以有参数和返回类型。普通参数作为值传递,而VAR参数通过引用传递。前面的块规则缺少的另一部分是statementSequence,只是单个语句的列表:\par 50 | 51 | \begin{tcolorbox}[colback=white,colframe=black] 52 | statementSequence \\ 53 | \hspace*{0.5cm}: statement ( ";" statement )* ; 54 | \end{tcolorbox} 55 | 56 | 如果语句后面跟着另一个语句,则该语句用分号分隔。同样,Modula-2语句支持这种方式:\par 57 | 58 | \begin{tcolorbox}[colback=white,colframe=black] 59 | statement \\ 60 | \hspace*{0.5cm}: qualident ( ":=" expression | ( "(" ( expList )? ")" )? ) \\ 61 | \hspace*{0.5cm}| ifStatement | whileStatement | "RETURN" ( expression )? ; 62 | \end{tcolorbox} 63 | 64 | 该规则的第一部分描述了一个赋值或过程调用。后跟:=的限定标识符是一个赋值。另一方面,如果后面跟着(,则它是一个过程调用。其他语句是常用的控制语句:\par 65 | 66 | \begin{tcolorbox}[colback=white,colframe=black] 67 | ifStatement \\ 68 | \hspace*{0.5cm}: "IF" expression "THEN" statementSequence \\ 69 | \hspace*{0.5cm}( "ELSE" statementSequence )? "END" ; 70 | \end{tcolorbox} 71 | 72 | IF语句也有一个简化的语法,因为它只能有一个ELSE块。可以通过这个语句,我们有条件地保护语句:\par 73 | 74 | \begin{tcolorbox}[colback=white,colframe=black] 75 | whileStatement \\ 76 | \hspace*{0.5cm}: "WHILE" expression "DO" statementSequence "END" ; 77 | \end{tcolorbox} 78 | 79 | WHILE语句描述了一个由条件保护的循环。加上IF语句,就可以用tinylang编写简单的算法。最后,没有表达式的定义:\par 80 | 81 | \begin{tcolorbox}[colback=white,colframe=black] 82 | expList \\ 83 | \hspace*{0.5cm}: expression ( "," expression )* ; \\ 84 | expression \\ 85 | \hspace*{0.5cm}: simpleExpression ( relation simpleExpression )? ; \\ 86 | relation \\ 87 | \hspace*{0.5cm}: "=" | "\#" | "<" | "<=" | ">" | ">=" ; \\ 88 | simpleExpression \\ 89 | \hspace*{0.5cm}: ( "+" | "-" )? term ( addOperator term )* ; \\ 90 | addOperator \\ 91 | \hspace*{0.5cm}: "+" | "-" | "OR" ; \\ 92 | term \\ 93 | \hspace*{0.5cm}: factor ( mulOperator factor )* ; \\ 94 | mulOperator \\ 95 | \hspace*{0.5cm}: "*" | "/" | "DIV" | "MOD" | "AND" ; \\ 96 | factor \\ 97 | \hspace*{0.5cm}: integer\underline{~}literal | "(" expression ")" | "NOT" factor \\ 98 | | qualident ( "(" ( expList )? ")" )? ; 99 | \end{tcolorbox} 100 | 101 | 表达式语法与前一章的calc相似,只支持INTEGER和BOOLEAN数据类型。\par 102 | 103 | 此外,还使用identifier和integer\underline{~}literal标记。\textbf{标识符}以字母或下划线开头,后面跟着字母、数字和下划线。\textbf{整数字面值}可以是一个十进制数字序列,也可以是一个十六进制数字序列,后面跟着字母H。\par 104 | 105 | 虽然已经介绍了很多规则,但只涉及Modula-2的一部分!尽管如此,还是可以使用这个子集中编写小型应用程序。接下来,让我们为tinylang实现一个编译器!\par 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /content/2/chapter4/3.tex: -------------------------------------------------------------------------------- 1 | tinylang的项目布局遵循了我们在第2章,LLVM源的访问中列出的方式。每个组件的源代码在lib目录的子目录中,而头文件在include/tinylang的子目录中,子目录以组件命名。在第2章,我们只创建了基本组件。\par 2 | 3 | 在前一章中,我们知道需要实现词法分析器、解析器、AST和语义分析器。其都有自己的组件,称为Lexer、Parser、AST和Sema。上一章中使用的目录布局如下:\par 4 | 5 | \hspace*{\fill} \par %插入空行 6 | \begin{center} 7 | \includegraphics{content/2/chapter4/images/1.jpg}\\ 8 | 图4.1 – tinylang项目的目录布局 9 | \end{center} 10 | 11 | 组件有明确定义的依赖项。在这里,Lexer只依赖于Basic。Parser依赖于Basic、Lexer、AST和Sema。最后,Sema只依赖于Basic和AST,这些定义良好的依赖有助于对组件的重用。\par 12 | 13 | 让我们仔细了解一下它们是如何实现的。\par 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /content/2/chapter4/4.tex: -------------------------------------------------------------------------------- 1 | 实际的编译器必须能够处理许多文件。通常,开发人员用主编译单元的名称来调用编译器。这个编译单元可以引用其他文件,例如:通过C语言中的\#include指令,或Python或Modula-2中的import语句。导入的模块可以导入其他模块等。所有这些文件都必须加载到内存中,并通过编译器的分析阶段运行。在开发过程中,开发人员可能会犯语法或语义错误。当检测到错误信息时,应该打印出错误信息,包括源行和一个标记,很明显,这个基本组件很重要。\par 2 | 3 | 幸运的是,LLVM附带了一个解决方案:LLVM::SourceMgr类。通过调用AddNewSourceBuffer()方法,将新源文件添加到SourceMgr。或者,通过调用AddIncludeFile()来加载文件。这两个方法都返回一个ID来标识缓冲区,可以使用这个ID来检索指向关联文件的内存缓冲区的指针。要在文件中定义位置,必须使用llvm::SMLoc类,这个类将指针封装到缓冲区中。各种PrintMessage()会向用户发出错误和其他信息消息。\par 4 | 5 | 只缺少一种集中定义消息的方法。大型软件(如编译器)中,您不希望到处都是消息字符串。如果有人要求更改消息或将其翻译成另一种语言,那么最好把它们放在中心位置!\par 6 | 7 | 一种简单的方法是,每个消息都有一个ID(一个enum成员)、一个严重性级别和一个包含消息的字符串。代码中,只引用消息ID。严重性级别和消息字符串仅在打印消息时使用。必须一致地管理这三个项(ID、安全级别和消息)。LLVM库使用一个预处理器来解决这个问题。数据存储在一个后缀为a.def的文件中,并封装在一个宏名称中。该文件通常包含多次,宏的定义不同。它的定义在include/tinylang/Basic/Diagnostic.def文件路径中,如下所示:\par 8 | 9 | \begin{lstlisting}[caption={}] 10 | #ifndef DIAG 11 | #define DIAG(ID, Level, Msg) 12 | #endif 13 | 14 | DIAG(err_sym_declared, Error, "symbol {0} already declared") 15 | #undef DIAG 16 | \end{lstlisting} 17 | 18 | 第一个宏参数ID是枚举标签,第二个参数Level是严重性,第三个参数Msg是消息文本。这样,就可以定义一个DiagnosticsEngine类发出的错误消息。该接口位于include/tinylang/Basic/Diagnostic.h文件中:\par 19 | 20 | \begin{lstlisting}[caption={}] 21 | #ifndef TINYLANG_BASIC_DIAGNOSTIC_H 22 | #define TINYLANG_BASIC_DIAGNOSTIC_H 23 | 24 | #include "tinylang/Basic/LLVM.h" 25 | #include "llvm/ADT/StringRef.h" 26 | #include "llvm/Support/FormatVariadic.h" 27 | 28 | #include "llvm/Support/SMLoc.h" 29 | #include "llvm/Support/SourceMgr.h" 30 | #include "llvm/Support/raw_ostream.h" 31 | #include 32 | 33 | namespace tinylang { 34 | \end{lstlisting} 35 | 36 | 包含必要的头文件之后,现在使用Diagnostic.def来定义枚举。为了不污染全局命名空间,需要使用命名空间diag:\par 37 | 38 | \begin{lstlisting}[caption={}] 39 | namespace diag { 40 | enum { 41 | #define DIAG(ID, Level, Msg) ID, 42 | #include "tinylang/Basic/Diagnostic.def" 43 | }; 44 | } // namespace diag 45 | \end{lstlisting} 46 | 47 | DiagnosticsEngine类使用SourceMgr实例通过report()发出消息,消息可以有参数。要实现这个功能,必须使用LLVM的可变格式支持。在静态方法的帮助下检索消息文本和严重性级别。此外,还会计算发出的错误消息的数量:\par 48 | 49 | \begin{lstlisting}[caption={}] 50 | class DiagnosticsEngine { 51 | static const char *getDiagnosticText(unsigned DiagID); 52 | static SourceMgr::DiagKind 53 | getDiagnosticKind(unsigned DiagID); 54 | \end{lstlisting} 55 | 56 | 消息字符串由getDiagnosticText()返回,级别由getDiagnosticKind()返回。这两种方法都将在以后的.cpp文件中实现:\par 57 | 58 | \begin{lstlisting}[caption={}] 59 | SourceMgr &SrcMgr; 60 | unsigned NumErrors; 61 | 62 | public: 63 | DiagnosticsEngine(SourceMgr &SrcMgr) 64 | : SrcMgr(SrcMgr), NumErrors(0) {} 65 | 66 | unsigned nunErrors() { return NumErrors; } 67 | \end{lstlisting} 68 | 69 | 因为消息可以有可变数量的参数,所以C++中的解决方案是使用可变参数模板。当然,LLVM提供的formatv()函数也使用这种方法。要获得格式化的消息,我们只需要转发模板参数:\par 70 | 71 | \begin{lstlisting}[caption={}] 72 | template 73 | void report(SMLoc Loc, unsigned DiagID, 74 | Args &&... Arguments) { 75 | std::string Msg = 76 | llvm::formatv(getDiagnosticText(DiagID), 77 | std::forward(Arguments)...) 78 | .str(); 79 | SourceMgr::DiagKind Kind = getDiagnosticKind(DiagID); 80 | SrcMgr.PrintMessage(Loc, Kind, Msg); 81 | NumErrors += (Kind == SourceMgr::DK_Error); 82 | } 83 | }; 84 | 85 | } // namespace tinylang 86 | 87 | #endif 88 | \end{lstlisting} 89 | 90 | 这样,就实现了类的大部分内容。只缺少getDiagnosticText()和getDiagnosticKind()。它们在lib/Basic/Diagnostic.cpp文件中定义,并使用Diagnostic.def文件:\par 91 | 92 | \begin{lstlisting}[caption={}] 93 | #include "tinylang/Basic/Diagnostic.h" 94 | using namespace tinylang; 95 | namespace { 96 | const char *DiagnosticText[] = { 97 | #define DIAG(ID, Level, Msg) Msg, 98 | #include "tinylang/Basic/Diagnostic.def" 99 | }; 100 | \end{lstlisting} 101 | 102 | 与头文件中一样,DIAG宏定义为检索所需的部分。这里,将定义一个数组来保存文本消息。因此,DIAG宏只返回Msg部分。我们将使用相同的方法:\par 103 | 104 | \begin{lstlisting}[caption={}] 105 | SourceMgr::DiagKind DiagnosticKind[] = { 106 | #define DIAG(ID, Level, Msg) SourceMgr::DK_##Level, 107 | #include "tinylang/Basic/Diagnostic.def" 108 | }; 109 | } // namespace 110 | \end{lstlisting} 111 | 112 | 不出所料,这两个函数都只是简单地对数组进行索引,并返回所需的数据:\par 113 | 114 | \begin{lstlisting}[caption={}] 115 | const char * 116 | DiagnosticsEngine::getDiagnosticText(unsigned DiagID) { 117 | return DiagnosticText[DiagID]; 118 | } 119 | 120 | SourceMgr::DiagKind 121 | DiagnosticsEngine::getDiagnosticKind(unsigned DiagID) { 122 | return DiagnosticKind[DiagID]; 123 | } 124 | \end{lstlisting} 125 | 126 | SourceMgr和DiagnosticsEngine类的这种组合为其他组件提供了良好的基础。让我们先在词法分析器中使用一下!\par 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /content/2/chapter4/5.tex: -------------------------------------------------------------------------------- 1 | 正如前一章中所知,我们需要一个Token类和一个Lexer类。此外,需要一个TokenKind枚举来给每个令牌类一个的数字。拥有一体化的头文件和一个实现文件是无法扩展的,所以需要重构这些内容。TokenKind枚举可以复用,并放置在Basic组件中。Token和Lexer类属于Lexer组件,但是放在不同的头文件和实现文件中。\par 2 | 3 | 有三种不同类型的标记:关键字、标点符号和表示多值集的标记,例如:CONST关键字,分号分隔符和标识符,它们表示源中的标识符,每个令牌都需要枚举的成员名。关键字和标点符号具有可以用于消息的显示名称。\par 4 | 5 | 与许多编程语言一样,关键字是标识符的子集。为了将标记分类为关键字,我们需要一个关键字过滤器,它检查所找到的标识符是否确实是关键字。这与C或C++中的行为相同,其中关键字也是标识符的子集。编程语言随着时间的推移而发展,可能会引入新的关键字,例如:K\&R C语言没有使用enum关键字定义的枚举。因此,应该有指示关键字的语言级别的标志。\par 6 | 7 | 我们收集了几条信息,它们都属于TokenKind枚举的一个成员:枚举成员的标签、标点符号的拼写和关键字的标志。至于调试消息,我们将信息集中存储在一个名为include/tinylang/Basic/Token\allowbreak Kinds.def的文件中,文件的内容如下所示。需要注意的是,关键字的前缀是kw\underline{~}:\par 8 | 9 | \begin{lstlisting}[caption={}] 10 | #ifndef TOK 11 | #define TOK(ID) 12 | #endif 13 | #ifndef PUNCTUATOR 14 | #define PUNCTUATOR(ID, SP) TOK(ID) 15 | #endif 16 | #ifndef KEYWORD 17 | #define KEYWORD(ID, FLAG) TOK(kw_ ## ID) 18 | #endif 19 | 20 | TOK(unknown) 21 | TOK(eof) 22 | TOK(identifier) 23 | TOK(integer_literal) 24 | 25 | PUNCTUATOR(plus, "+") 26 | PUNCTUATOR(minus, "-") 27 | // … 28 | 29 | KEYWORD(BEGIN , KEYALL) 30 | KEYWORD(CONST , KEYALL) 31 | // … 32 | 33 | #undef KEYWORD 34 | #undef PUNCTUATOR 35 | #undef TOK 36 | \end{lstlisting} 37 | 38 | 有了这些集中的定义,就很容易在include/tinylang/Basic/TokenKinds.h中创建TokenKind枚举。同样,枚举也可以放在自己的命名空间tok中:\par 39 | 40 | \begin{lstlisting}[caption={}] 41 | #ifndef TINYLANG_BASIC_TOKENKINDS_H 42 | #define TINYLANG_BASIC_TOKENKINDS_H 43 | 44 | namespace tinylang { 45 | 46 | namespace tok { 47 | enum TokenKind : unsigned short { 48 | #define TOK(ID) ID, 49 | #include "TokenKinds.def" 50 | NUM_TOKENS 51 | }; 52 | \end{lstlisting} 53 | 54 | 填充数组使用的模式已经很熟悉了,TOK宏定义为仅返回枚举标签的ID。作为一个有用的加法,我们还将NUM\underline{~}TOKENS定义为枚举的最后一个成员,表示已定义标记的数量:\par 55 | 56 | \begin{lstlisting}[caption={}] 57 | const char *getTokenName(TokenKind Kind); 58 | const char *getPunctuatorSpelling(TokenKind Kind); 59 | const char *getKeywordSpelling(TokenKind Kind); 60 | } 61 | } 62 | 63 | #endif 64 | \end{lstlisting} 65 | 66 | 实现文件lib/Basic/TokenKinds.cpp也使用.def文件来检索名称:\par 67 | 68 | \begin{lstlisting}[caption={}] 69 | #include "tinylang/Basic/TokenKinds.h" 70 | #include "llvm/Support/ErrorHandling.h" 71 | 72 | using namespace tinylang; 73 | 74 | static const char * const TokNames[] = { 75 | #define TOK(ID) #ID, 76 | #define KEYWORD(ID, FLAG) #ID, 77 | #include "tinylang/Basic/TokenKinds.def" 78 | nullptr 79 | }; 80 | \end{lstlisting} 81 | 82 | 令牌的文本名称派生自其枚举标签的ID,其有两个特点。首先,需要定义两个TOK和KEYWORD宏,因为KEYWORD的默认定义不使用TOK宏。其次,会在数组的末尾添加一个nullptr值,用于计算NUM\underline{~}TOKENS枚举成员:\par 83 | 84 | \begin{lstlisting}[caption={}] 85 | const char *tok::getTokenName(TokenKind Kind) { 86 | return TokNames[Kind]; 87 | } 88 | \end{lstlisting} 89 | 90 | 对于getPunctuatorSpelling()getKeywordSpelling()函数,我们采用了不同的方法。这些函数只返回枚举子集的有意义的值。可以通过switch语句实现,该语句默认返回nullptr值:\par 91 | 92 | \begin{lstlisting}[caption={}] 93 | const char *tok::getPunctuatorSpelling(TokenKind Kind) { 94 | switch (Kind) { 95 | #define PUNCTUATOR(ID, SP) case ID: return SP; 96 | #include "tinylang/Basic/TokenKinds.def" 97 | default: break; 98 | } 99 | return nullptr; 100 | } 101 | 102 | const char *tok::getKeywordSpelling(TokenKind Kind) { 103 | switch (Kind) { 104 | #define KEYWORD(ID, FLAG) case kw_ ## ID: return #ID; 105 | #include "tinylang/Basic/TokenKinds.def" 106 | default: break; 107 | } 108 | return nullptr; 109 | } 110 | \end{lstlisting} 111 | 112 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black,title=Tip] 113 | 注意如何定义宏从文件中检索所需的信息片段。 114 | \end{tcolorbox} 115 | 116 | 前一章中,Token类在与Lexer类相同的头文件中声明。为了使其更加模块化,我们将把Token类放到include/Lexer/Token.h中。前面的例子中,Token存储了一个指针,指向标记的开始、长度和标记的类型:\par 117 | 118 | \begin{lstlisting}[caption={}] 119 | class Token { 120 | friend class Lexer; 121 | 122 | const char *Ptr; 123 | size_t Length; 124 | tok::TokenKind Kind; 125 | 126 | public: 127 | tok::TokenKind getKind() const { return Kind; } 128 | size_t getLength() const { return Length; } 129 | \end{lstlisting} 130 | 131 | SMLoc实例表示源在消息中的位置,它是使用指向令牌的指针创建:\par 132 | 133 | \begin{lstlisting}[caption={}] 134 | SMLoc getLocation() const { 135 | return SMLoc::getFromPointer(Ptr); 136 | } 137 | \end{lstlisting} 138 | 139 | getIdentifier()和getLiteralData()允许我们访问标识符和文字数据的标记文本。没有必要访问任何其他令牌类型的文本,因为这是隐式的令牌类型:\par 140 | 141 | \begin{lstlisting}[caption={}] 142 | StringRef getIdentifier() { 143 | assert(is(tok::identifier) && 144 | "Cannot get identfier of non-identifier"); 145 | return StringRef(Ptr, Length); 146 | } 147 | StringRef getLiteralData() { 148 | assert(isOneOf(tok::integer_literal, 149 | tok::string_literal) && 150 | "Cannot get literal data of non-literal"); 151 | return StringRef(Ptr, Length); 152 | } 153 | }; 154 | \end{lstlisting} 155 | 156 | 我们在include/Lexer/Lexer.h头文件中声明Lexer类,并将实现放在lib/Lexer/Lexer.cpp文件中。其结构与前一章的calc语言相同。这里,我们必须留意两个细节:\par 157 | 158 | \begin{itemize} 159 | \item 首先,有些操作符共享相同的前缀,例如:<和<=。当我们正在查看的当前字符是<时,首先检查下一个字符,然后再决定我们找到了哪个标记。记住,我们要求输入以空字节结束。因此,如果当前字符有效,则可以使用下一个字符: 160 | \begin{lstlisting}[caption={}] 161 | case '<': 162 | if (*(CurPtr + 1) == '=') 163 | formTokenWithChars(token, CurPtr + 2, tok::lessequal); 164 | else 165 | formTokenWithChars(token, CurPtr + 1, tok::less); 166 | break; 167 | \end{lstlisting} 168 | 169 | \item 另一个细节是,如有更多的关键词,我们该如何处理?一个简单而快速的解决方案是用关键字填充哈希表,这些关键字都存储在tokenkind.def文件中,可在实例化Lexer类时完成。这种方法还可以支持不同级别的语言,因为可以用附加的标志过滤关键字。这里,还不需要这种灵活性。头文件中,关键字过滤器定义如下,使用一个实例llvm::StringMap的哈希表: 170 | \begin{lstlisting}[caption={}] 171 | class KeywordFilter { 172 | llvm::StringMap HashTable; 173 | void addKeyword(StringRef Keyword, 174 | tok::TokenKind TokenCode); 175 | public: 176 | void addKeywords(); 177 | \end{lstlisting} 178 | 179 | getKeyword()方法返回给定字符串的标记类型,如果字符串不代表关键字,则返回默认值: 180 | \begin{lstlisting}[caption={}] 181 | tok::TokenKind getKeyword( 182 | StringRef Name, 183 | tok::TokenKind DefaultTokenCode = tok::unknown) { 184 | auto Result = HashTable.find(Name); 185 | if (Result != HashTable.end()) 186 | return Result->second; 187 | return DefaultTokenCode; 188 | } 189 | }; 190 | \end{lstlisting} 191 | 192 | 在实现文件中,关键字表填充为: 193 | \begin{lstlisting}[caption={}] 194 | void KeywordFilter::addKeyword(StringRef Keyword, 195 | tok::TokenKind TokenCode) 196 | { 197 | HashTable.insert(std::make_pair(Keyword, TokenCode)); 198 | } 199 | 200 | void KeywordFilter::addKeywords() { 201 | #define KEYWORD(NAME, FLAGS) 202 | addKeyword(StringRef(#NAME), tok::kw_##NAME); 203 | #include "tinylang/Basic/TokenKinds.def" 204 | } 205 | \end{lstlisting} 206 | 207 | \end{itemize} 208 | 209 | 使用这些技术,编写一个高效的lexer类并不困难。由于编译速度很重要,许多编译器都使用手写的分析器,Clang就是其中之一。\par 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | -------------------------------------------------------------------------------- /content/2/chapter4/6.tex: -------------------------------------------------------------------------------- 1 | 如前一章所示,解析器是从语法派生出来的。让我们回顾一下所有的构造规则:对于每个语法规则,都要创建一个以规则左侧的非终结符命名的方法,以便解析规则的右侧。根据右边的定义,必须做以下事情:\par 2 | 3 | \begin{itemize} 4 | \item 对于每个非终结符,调用相应的方法。 5 | \item 处理每个令牌。 6 | \item 对于可选组和可选组或重复组,将检查前瞻性令牌(下一个未使用的令牌),以决定从哪里继续。 7 | \end{itemize} 8 | 9 | 让我们将这些结构规则应用到以下语法规则中:\par 10 | 11 | \begin{tcolorbox}[colback=white,colframe=black] 12 | ifStatement \\ 13 | \hspace*{0.5cm}: "IF" expression "THEN" statementSequence \\ 14 | \hspace*{1cm}( "ELSE" statementSequence )? "END" ; 15 | \end{tcolorbox} 16 | 17 | 可以很容易地将其转换为以下C++方法:\par 18 | 19 | \begin{lstlisting}[caption={}] 20 | void Parser::parseIfStatement() { 21 | consume(tok::kw_IF); 22 | parseExpression(); 23 | consume(tok::kw_THEN); 24 | parseStatementSequence(); 25 | if (Tok.is(tok::kw_ELSE)) { 26 | advance(); 27 | parseStatementSequence(); 28 | } 29 | consume(tok::kw_END); 30 | } 31 | \end{lstlisting} 32 | 33 | tinylang的整个语法可以用这种方式转换成C++。不过,必须小心并避免一些陷阱。\par 34 | 35 | 需要注意的一个问题是左递归规则。如果右边和左边以相同的终端开始,规则是左递归的。表达式的语法中可以找到一个典型的例子:\par 36 | 37 | \begin{tcolorbox}[colback=white,colframe=black] 38 | expression : expression "+" term ; 39 | \end{tcolorbox} 40 | 41 | 如果还不清楚语法,那么下面对C++的翻译应该可以清楚地表明,这将导致无限递归:\par 42 | 43 | \begin{lstlisting}[caption={}] 44 | void Parser::parseExpression() { 45 | parseExpression(); 46 | consume(tok::plus); 47 | parseTerm(); 48 | } 49 | \end{lstlisting} 50 | 51 | 左递归也可能间接发生,涉及更多规则,这很难发现。这就是为什么存在一种可以检测和消除左递归的算法。\par 52 | 53 | 每个步骤中,解析器决定如何仅通过使用预先标记继续。如果不能确定,语法就会发生冲突。为了说明这一点,让我们看看C\#中的using语句。与C++类似,using语句可用于在名称空间中使符号可见,例如在using Math;中。还可以为导入的符号定义别名;也就是说,使用\textit{M = Math;}在语法中,可以这样表示:\par 54 | 55 | \begin{tcolorbox}[colback=white,colframe=black] 56 | usingStmt : "using" (ident "=")? ident ";" 57 | \end{tcolorbox} 58 | 59 | 显然,这里有个问题。解析器使用了using关键字之后,就标识了预先标记。但是,这些信息不足以让我们决定是否必须跳过或解析可选组。如果可选组开始的一组标记与可选组后面的一组标记重叠,则总是会出现这种情况。\par 60 | 61 | 我们用一个替代方案来重写这个规则:\par 62 | 63 | 64 | \begin{tcolorbox}[colback=white,colframe=black] 65 | usingStmt : "using" ( ident "=" ident | ident ) ";" ; 66 | \end{tcolorbox} 67 | 68 | 现在,有一个不同的冲突:两个选择都以相同的标记开始。只看预先标记,解析器不能决定哪个选项是正确的。\par 69 | 70 | 这些冲突非常普遍。因此,需要了解如何处理它们。一种方法是以冲突消失的方式重写语法。在前面的示例中,两个选项都以相同的标记开始。这可以分解,得到以下规则:\par 71 | 72 | \begin{tcolorbox}[colback=white,colframe=black] 73 | usingStmt : "using" ident ("=" ident)? ";" ; 74 | \end{tcolorbox} 75 | 76 | 这种表示没有冲突。然而,还应该注意的是,它的表达性较差。其他两个公式中,很明显哪个标识是别名,哪个标识是名称空间名称。这个无冲突规则中,最左边的标识改变了它的角色。首先,是名称空间名称,但是如果后面跟着等号(=),那么就变成别名。\par 77 | 78 | 第二种方法是添加一个额外的谓词来区分这两种情况。这个谓词通常称为解释器,它可以使用上下文信息进行决策(例如符号表中的名称查找),但可以查看多个令牌。让我们假设lexer有一个\textit{Token \&peek(int n)}方法,在当前查找令牌之后返回第n个令牌。这里,等号的存在可以作为判定中的附加谓词:\par 79 | 80 | \begin{lstlisting}[caption={}] 81 | if (Tok.is(tok::ident) && Lex.peek(0).is(tok::equal)) { 82 | advance(); 83 | consume(tok::equal); 84 | } 85 | consume(tok::ident); 86 | \end{lstlisting} 87 | 88 | 现在,让我们加入错误恢复。前一章中,介绍了\textit{恐慌模式}作为一种错误恢复技术。基本思想是跳过标记,直到找到适合继续解析的标记为止。例如,在tinylang中,语句后面跟着分号(:)。\par 89 | 90 | 如果If语句中存在语法问题,则跳过所有标记,直到找到分号。然后,继续下一个。与其为令牌集的特别定义标记,不如使用系统方法。\par 91 | 92 | 对于每个非终结符,计算可以在跟随该非终结符的标记集(称为FOLLOW集)。对于非终结符语句,可以跟;、ELSE和END标记。因此,可以在parseStatement()的错误恢复部分中使用这个集合。当然,此方法假设语法错误可以在本地处理。通常来说,这不太可能。因为解析器会跳过标记,所以可能会跳过很多标记,以至于到达输入的末尾。所以,无法进行本地恢复。\par 93 | 94 | 为了防止无意义的错误消息,需要通知调用方法错误恢复仍未完成。这可以通过bool返回值来完成:true表示错误恢复尚未完成,false表示解析(包括可能的错误恢复)成功。\par 95 | 96 | 有许多方法可以扩展这个错误恢复方案,一种流行的方法是也使用活动调用者的FOLLOW集合。一个简单的例子,让我们假设parseStatement()由parseStatementSequence()调用,而parseBlock()本身调用parseBlock(),而parseModule()调用parseStatement()。\par 97 | 98 | 这里,每个对应的非终端都有一个FOLLOW集。如果解析器检测到parseStatement()中的语法错误,那么将跳过令牌,直到令牌至少出现在活动调用者的一个FOLLOW集合中。如果令牌在语句的FOLLOW集合中,则在本地恢复错误,并将一个假值返回给调用者。否则,返回一个真值,这意味着错误恢复必须继续。这个扩展的一个可能的实现策略是,传递std::bitset或std::tuple来表示当前FOLLOW的集合返回给调用者。\par 99 | 100 | 最后一个问题仍然未解决:如何调用错误恢复?前一章中,使用goto跳转到错误恢复块。这可行,但不是很优雅的解决方案。根据前面的讨论,可以在单独的方法中跳过标记。为此,Clang有一个名为skipUntil()的方法,我们也可以将其用于tinylang。\par 101 | 102 | 因为下一步是向解析器添加语义操作,所以如果有必要,最好有找个位置完成清理代码(嵌套函数是不错的选择)。C++没有嵌套函数,所以Lambda函数可以用于这项任务。具有错误恢复的parseIfStatement()如下所示:\par 103 | 104 | \begin{lstlisting}[caption={}] 105 | bool Parser::parseIfStatement() { 106 | auto _errorhandler = [this] { 107 | return SkipUntil(tok::semi, tok::kw_ELSE, tok::kw_END); 108 | }; 109 | if (consume(tok::kw_IF)) 110 | return _errorhandler(); 111 | if (parseExpression(E)) 112 | return _errorhandler(); 113 | if (consume(tok::kw_THEN)) 114 | return _errorhandler(); 115 | if (parseStatementSequence(IfStmts)) 116 | return _errorhandler(); 117 | if (Tok.is(tok::kw_ELSE)) { 118 | advance(); 119 | if (parseStatementSequence(ElseStmts)) 120 | return _errorhandler(); 121 | } 122 | if (expect(tok::kw_END)) 123 | return _errorhandler(); 124 | return false; 125 | } 126 | \end{lstlisting} 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | -------------------------------------------------------------------------------- /content/2/chapter4/7.tex: -------------------------------------------------------------------------------- 1 | 手动构造词法分析器和解析器并不困难,通常会产生处理速度很快的组件。但缺点是不容易更改,特别是在解析器中。如果正在创建一种新的编程语言的原型,那么这一点非常重要。使用特定的工具可以缓解这个问题。\par 2 | 3 | 有许多工具可以根据规范文件生成词法分析器或解析器。在Linux中,flex (\url{https://github.com/westes/flex})和bison (\url{https://www.gnu.org/software/bison/})是常用工具。Flex从一组正则表达式生成分析器,而bison从语法描述生成解析器。通常,这两种工具会一起使用。\par 4 | 5 | Bison根据语法描述生成LALR(1)解析器。LALR(1)解析器是一种自下而上的解析器,并且使用自动机实现。bison的输入是一个语法文件,与本章开始介绍的语法文件非常相似。主要的区别是右侧规则不支持正则表达式,可选的组和重复检查必须为规则重写。tinylang的bison规范,存储在tinylang.yy文件中,以以下内容开始:\par 6 | 7 | \begin{tcolorbox}[colback=white,colframe=black] 8 | \%require "3.2" \\ 9 | \%language "c++" \\ 10 | \%defines "Parser.h" \\ 11 | \%define api.namespace {tinylang} \\ 12 | \%define api.parser.class {Parser} \\ 13 | \%define api.token.prefix {T\underline{~}} \\ 14 | \%token \\ 15 | identifier integer\underline{~}literal string\underline{~}literal \\ 16 | PLUS MINUS STAR SLASH 17 | \end{tcolorbox} 18 | 19 | 我们指示bison使用\%language指令生成C++代码。使用\%define指令,重写了代码生成的一些默认值:生成的类应该命名为Parser,并位于tinylang命名空间中。此外,表示标记类型的枚举的成员应该以T\underline{~}作为前缀。我们需要3.2或更高版本,因为其中一些变量在这个版本中引入。为了能够与flex交互,我们告诉bison用\%define指令编写一个Parser.h头文件。最后,我们必须使用\%token指令声明所有已使用的令牌。\%\%后面是具体的语法规则:\par 20 | 21 | \begin{tcolorbox}[colback=white,colframe=black] 22 | \%\% \\ 23 | compilationUnit \\ 24 | \hspace*{0.5cm}: MODULE identifier SEMI imports block identifier PERIOD ; \\ 25 | imports : \%empty | import imports ; \\ 26 | import \\ 27 | \hspace*{0.5cm}: FROM identifier IMPORT identList SEMI \\ 28 | \hspace*{0.5cm}| IMPORT identList SEMI ; 29 | \end{tcolorbox} 30 | 31 | 请将这些规则与本章第一节所示的语法规范进行比较。bison不知道重复组,因此需要添加一个名为imports的新规则来对这种重复。在导入规则中,必须引入一个替代方法来对可选组建模。\par 32 | 33 | 我们还需要用这种风格重写tinylang语法的其他规则。例如,IF语句的规则如下:\par 34 | 35 | \begin{tcolorbox}[colback=white,colframe=black] 36 | ifStatement \\ 37 | \hspace*{0.5cm}: IF expression THEN statementSequence \\ 38 | \hspace*{1cm}elseStatement END ; \\ 39 | elseStatement : \%empty | ELSE statementSequence ; 40 | \end{tcolorbox} 41 | 42 | 同样,我们必须引入一个新的规则来处理可选的ELSE语句。可以省略\%empty指令,不过这里使用的是空分支。\par 43 | 44 | 当我们用bison风格重写了所有语法规则,就可以用以下命令生成解析器:\par 45 | 46 | \begin{tcolorbox}[colback=white,colframe=black] 47 | \$ bison tinylang.yy 48 | \end{tcolorbox} 49 | 50 | 这就是创建与上一节中手写解析器类似解析器的全部内容!\par 51 | 52 | 类似地,flex很容易使用。flex的规范是一个正则表达式和相关操作的列表,如果正则表达式匹配,则执行该列表。tinylang.l文件指定tinylang的词法分析器。与bison规范一样,也有一个标准开头:\par 53 | 54 | \begin{tcolorbox}[colback=white,colframe=black] 55 | \%{ \\ 56 | \#include "Parser.h" \\ 57 | \%} \\ 58 | \%option noyywrap nounput noinput batch \\ 59 | id [a-zA-Z\underline{~}][a-zA-Z\underline{~}0-9]* \\ 60 | digit [0-9] \\ 61 | hexdigit [0-9A-F] \\ 62 | space [ $\setminus$t$\setminus$r] 63 | \end{tcolorbox} 64 | 65 | 在\%\{\}\%里面的文本复制到flex生成的文件中,我们使用这种机制来包含由bison生成的头文件。使用\%option指令,我们控制生成的解析器应该具有哪些特性。只读取一个文件,并且在读取完一个文件后不需要继续读取另一个文件,所以可以指定noyywrap来禁用这个特性。我们也不需要访问底层的文件流,使用nounput和noinout禁用它。因为我们已经不需要一个交互式的解析器了,所以需要生成一个批扫描程序。\par 66 | 67 | 在开头的内容中,我们还可以定义字符模式以供以后使用。在\%\%后面跟着定义部分:\par 68 | 69 | \begin{tcolorbox}[colback=white,colframe=black] 70 | \%\% \\ 71 | {space}+ \\ 72 | {digit}+ \hspace{1 cm}return \\ 73 | \hspace*{3 cm}tinylang::Parser::token::T\underline{~}integer\underline{~}literal; 74 | \end{tcolorbox} 75 | 76 | 定义部分中,指定正则表达式模式和在模式与输入匹配时执行的操作。动作也可以是空的。\par 77 | 78 | \{space\}+模式使用序言中定义的空格字符模式,它匹配一个或多个空格字符。我们没有定义任何操作,因此将忽略所有空白。\par 79 | 80 | 要匹配一个数字,我们使用{digit}+模式。作为一个动作,我们只返回关联的令牌类型。对所有令牌都做了同样的操作。例如,对算术运算符执行以下操作:\par 81 | 82 | \begin{tcolorbox}[colback=white,colframe=black] 83 | "+" \hspace{2cm}return tinylang::Parser::token::T\underline{~}PLUS; \\ 84 | "-" \hspace{2cm}return tinylang::Parser::token::T\underline{~}MINUS; \\ 85 | "*" \hspace{2cm}return tinylang::Parser::token::T\underline{~}STAR; \\ 86 | "/" \hspace{2cm}return tinylang::Parser::token::T\underline{~}SLASH; 87 | \end{tcolorbox} 88 | 89 | 如果有几个模式匹配输入,则选择匹配时间最长的模式。如果仍然有多个模式与输入匹配,那么将选择规范文件中按字典顺序最先出现的模式。这就是为什么必须首先定义关键字的模式,而只在所有关键字之后定义标识符模式:\par 90 | 91 | \begin{tcolorbox}[colback=white,colframe=black] 92 | "VAR" \hspace{2cm}return tinylang::Parser::token::T\underline{~}VAR; \\ 93 | "WHILE" \hspace{2cm}return tinylang::Parser::token::T\underline{~}WHILE; \\ 94 | {id} \hspace{2cm}return tinylang::Parser::token::T\underline{~}identifier; 95 | \end{tcolorbox} 96 | 97 | 这些操作不仅仅局限于return语句。如果您的代码有多行,那么您必须用大括号括起来\{\}。\par 98 | 99 | 扫描通过以下命令生成:\par 100 | 101 | \begin{tcolorbox}[colback=white,colframe=black] 102 | \$ flex –c++ tinylang.l 103 | \end{tcolorbox} 104 | 105 | 在语言项目中你应该使用哪种方法?解析器生成器通常生成LALR(1)解析器。LALR(1)类比LL(1)类大,可以为其构造递归下降解析器。如果不能调整语法以适应LL(1)类,那么应该考虑使用解析器生成器,手工构造一个自底向上的解析器是不可行的。即使语法是LL(1),解析器生成器在生成与您手工编写代码类似的代码时也会提供更多的便利。通常,这受许多因素影响的选择。Clang使用手写解析器,而GCC使用bison生成的解析器。\par 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /content/2/chapter4/9.tex: -------------------------------------------------------------------------------- 1 | 本章中,您学习了在前端使用的编译器技术。从项目的布局开始,您为词法分析器、解析器和语义分析器创建了单独的库。为了向用户输出消息,您扩展了现有的LLVM类,允许集中存储消息。词法分析器目前已经分成几个接口。\par 2 | 3 | 然后,您学习了如何根据语法描述构造递归下降解析器、要避免哪些缺陷,以及如何使用生成器来完成这项工作。您构建的语义分析器执行语言所需的所有语义检查,同时与解析器和AST构造交织在一起。\par 4 | 5 | 编码工作的结果是一个修饰过的AST,下一章中将使用它生成IR代码和目标代码。\par 6 | 7 | \newpage -------------------------------------------------------------------------------- /content/2/chapter4/images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/2/chapter4/images/1.jpg -------------------------------------------------------------------------------- /content/2/chapter5/0.tex: -------------------------------------------------------------------------------- 1 | 在为编程语言创建了修饰的\textbf{抽象语法树(AST)}之后,下一个任务是从它生成LLVM IR代码。LLVM IR代码类似于三地址代码,具有人类可读的表示形式。因此,我们需要一种系统的方法,将诸如控制结构之类的语言概念翻译到较底层次的LLVM IR中。\par 2 | 3 | 本章中,您将学习LLVM IR的基础知识,以及如何从AST中为控制流结构生成IR。还将学习如何使用现代算法,为\textbf{静态单分配(SSA)形式}的表达式生成LLVM IR。最后,您将学习如何生成汇编文本和目标代码。\par 4 | 5 | 本章将涵盖以下内容:\par 6 | 7 | \begin{itemize} 8 | \item 使用AST生成IR 9 | \item 使用AST编码生成SSA形式的IR代码 10 | \item 设置模块和驱动 11 | \end{itemize} 12 | 13 | 本章结束时,将了解为自己的编程语言创建代码生成器的知识,以及如何将它集成到自己的编译器中。\par 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /content/2/chapter5/1.tex: -------------------------------------------------------------------------------- 1 | 本章的代码文件可在\url{https://github.com/PacktPublishing/Learn-LLVM-12/tree/master/Chapter05/tinylang}获取。\par 2 | 3 | 你可以在视频中找到代码\url{https://bit.ly/3nllhED}。\par -------------------------------------------------------------------------------- /content/2/chapter5/2.tex: -------------------------------------------------------------------------------- 1 | LLVM代码生成器接受IR中描述的模块作为输入,并将其转换为目标代码或汇编文本,我们需要将AST表示转换为IR。为了实现IR代码生成器,我们将首先查看一个简单的示例,然后开发代码生成器所需的类。完整的实现将分为三个类:CodeGenerator、CGModule和CGProcedure类。CodeGenerator类是编译器驱动程序使用的通用接口。CGModule和CGProcedure类保存了为编译单元和单个函数生成IR代码所需的状态。\par 2 | 3 | 下一节中,我们首先看看Clang生成的IR。\par 4 | 5 | 6 | \hspace*{\fill} \par %插入空行 7 | \textbf{了解IR代码} 8 | 9 | 生成IR代码之前,最好先了解IR语言的主要元素。在第3章中,我们已经简要地了解了IR。获得更多IR的一个简单方法是研究clang的输出。例如,保存以下C源代码,它实现了计算两个数的最大公约数的欧几里得算法:\par 10 | 11 | \begin{lstlisting}[caption={}] 12 | unsigned gcd(unsigned a, unsigned b) { 13 | if (b == 0) 14 | return a; 15 | while (b != 0) { 16 | unsigned t = a % b; 17 | a = b; 18 | b = t; 19 | } 20 | return a; 21 | } 22 | \end{lstlisting} 23 | 24 | 然后可以使用以下命令,创建IR文件gcd.ll:\par 25 | 26 | \begin{tcolorbox}[colback=white,colframe=black] 27 | \$ clang --target=aarch64-linux-gnu –O1 -S -emit-llvm gcd.c 28 | \end{tcolorbox} 29 | 30 | IR代码与目标有关,该命令用于编译Linux环境下ARM 64位CPU的源文件。-S选项指示clang输出一个程序集文件,通过附加的-emit-llvm,创建一个IR文件。优化级别-O1用于获得易读的IR代码。看一下生成的文件,并理解C源代码如何映射到IR。在文件的顶部,有一些基本信息:\par 31 | 32 | \begin{tcolorbox}[colback=white,colframe=black] 33 | ; ModuleID = 'gcd.c' \\ 34 | source\underline{~}filename = "gcd.c" \\ 35 | target datalayout = "e-m:e-i8:8:32-i16:16:32-i64:64- \\ 36 | \hspace*{3.5cm}i128:128-n32:64-S128" \\ 37 | target triple = "aarch64-unknown-linux-gnu" 38 | \end{tcolorbox} 39 | 40 | 第一行是注释,说明使用了哪个模块标识符。下一行中,注明了源文件的文件名。对于clang,两者一样。\par 41 | 42 | target datalayout字符串建立了一些基本属性。它的各个部分用-隔开。包括以下信息:\par 43 | 44 | \begin{itemize} 45 | \item 小e意味着内存中的字节使用小端模式存储。要指定大的端序,可以使用大写的E。 46 | \item 指定应用于符号的名称转换。这里,m:e表示使用ELF名称mangling。 47 | \item 在iN:A:P形式中的条目i8:8:32,指定了数据的对齐方式,以位为单位。第一个数字是ABI所需的对齐方式,第二个数字是首选的对齐方式。对于bytes (i8), ABI对齐是1字节(8),首选对齐是4字节(32)。 48 | \item n指定可用的本机寄存器大小。n32:64意味着本地支持32位和64位宽整数。 49 | \item s指定堆栈的对齐方式,同样以位为单位。S128表示堆栈保持16字节对齐。 50 | \end{itemize} 51 | 52 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black,title=Note] 53 | 目标数据的更多的信息,你以在参考手册\url{https://llvm.org/docs/LangRef.html#data-layout}。 54 | \end{tcolorbox} 55 | 56 | 最后,target triple字符串指定了我们要编译的体系结构。对于我们在命令行中给出的信息来说,这是必不可少的。这个在第2章,已经进行了深入的讨论。\par 57 | 58 | 接下来,在IR文件中定义gcd函数:\par 59 | 60 | \begin{tcolorbox}[colback=white,colframe=black] 61 | define i32 @gcd(i32 \%a, i32 \%b) \{ 62 | \end{tcolorbox} 63 | 64 | 这类似于C文件中的函数签名。unsigned数据类型被转换为32位整数类型i32。函数名以@作为前缀,参数名以\%作为前缀。函数体用大括号括起来。正文代码如下:\par 65 | 66 | \begin{tcolorbox}[colback=white,colframe=black] 67 | entry: \\ 68 | \hspace*{0.5cm}\%cmp = icmp eq i32 \%b, 0 \\ 69 | \hspace*{0.5cm}br i1 \%cmp, label \%return, label \%while.body 70 | \end{tcolorbox} 71 | 72 | IR代码组织在基本块中。结构良好的\textbf{基本块}是指令的线性序列,它以可选标签开始,以终止指令(例如,分支或返回指令)结束。因此,每个基本块都有一个入口点和一个出口点。LLVM允许在构造时出现畸形的基本块。第一个基本块的标签是entry。块中的代码很简单:第一个指令比较参数\%b和0。第二个指令分支到label return,如条件为真,则分支到label while;若条件为假,则返回.\par 73 | 74 | IR代码的另一个特点是\textit{SSA}形式的。代码使用无限数量的虚拟寄存器,但每个寄存器只编写一次。比较的结果分配给指定的虚拟寄存器\%cmp。这个寄存器随后会使用,但它永远不会再写入。诸如常量传播和公共子表达式消除之类的优化在SSA表单中工作得非常好,所有现代编译器都在使用它。\par 75 | 76 | 下一个基本块是while的循环体:\par 77 | 78 | \begin{tcolorbox}[colback=white,colframe=black] 79 | while.body: \\ 80 | \hspace*{0.5cm}\%b.addr.010 = phi i32 [ \%rem, \%while.body ], \\ 81 | \hspace*{3.5cm}[ \%b, \%entry ] \\ 82 | \hspace*{0.5cm}\%a.addr.09 = phi i32 [ \%b.addr.010, \%while.body ], \\ 83 | \hspace*{3.5cm}[ \%a, \%entry ] \\ 84 | \hspace*{0.5cm}\%rem = urem i32 \%a.addr.09, \%b.addr.010 \\ 85 | \hspace*{0.5cm}\%cmp1 = icmp eq i32 \%rem, 0 \\ 86 | \hspace*{0.5cm}br i1 \%cmp1, label \%return, label \%while.body 87 | \end{tcolorbox} 88 | 89 | 在gcd循环中,赋于a和b参数以新值。如果一个寄存器只能写入一次,那么是无法完成的。解决方法是使用特殊的phi指令,phi指令有一个基本块列表和值作为参数。一个基本块表示从哪边的基本块进入的,值就是那些基本块的值。在运行时,phi指令将前面执行的基本块的标签与参数列表中的标签进行比较。\par 90 | 91 | 指令的值与标签的值相关联。对于第一个phi指令,如果之前执行的基本块是while.body,则值为\%rem。如果entry是前面执行的基本块,则该值为\%b。这些值位于基本块的开始部分。\%b.addr.010从第一个phi指令中得到一个值。在第二个phi指令的参数列表中使用了相同的寄存器,但该值假定为通过第一个phi指令改变它之前的值。\par 92 | 93 | 在循环体之后,必须选择返回值:\par 94 | 95 | \begin{tcolorbox}[colback=white,colframe=black] 96 | return: \\ 97 | \hspace*{0.5cm}\%retval.0 = phi i32 [ \%a, \%entry ], \\ 98 | \hspace*{3.5cm}[ \%b.addr.010, \%while.body ] \\ 99 | \hspace*{0.5cm}ret i32 \%retval.0 \\ 100 | \} 101 | \end{tcolorbox} 102 | 103 | 同样,使用phi指令来选择所需的值。ret指令不仅可以结束这个基本块,还表示该函数在运行时的结束。它将返回值作为参数。\par 104 | 105 | 对于phi指令的使用有一些限制,必须是基本块的第一个指令。第一个基本块是特殊的:没有块在它之前执行过。因此,不能以phi指令开始。\par 106 | 107 | IR代码本身看起来很像C语言和汇编语言的混合。尽管风格类似,我们还不清楚如何从AST轻松生成IR代码。特别是phi指令看起来很难生成,在下一节中,我们将介绍一个简单的算法来实现这一点!\par 108 | 109 | \hspace*{\fill} \par %插入空行 110 | \textbf{了解加载-存储} 111 | 112 | LLVM中的所有本地优化都基于这里显示的SSA形式。对于全局变量,使用内存引用。IR语言知道用于获取和存储这些值的加载和存储指令,也可以将此用于局部变量。这些指令不是SSA形式的,LLVM知道如何将它们转换成所需的SSA形式。因此,可以为每个局部变量分配内存,并使用加载-存储指令更改它们的值。您只需要记住指向存储变量的内存的指针,clang编译器使用的就是这种方法。\par 113 | 114 | 让我们看看带有加载和存储的IR代码。再次编译gcd.c,这次不启用优化:\par 115 | 116 | \begin{tcolorbox}[colback=white,colframe=black] 117 | \$ clang --target=aarch64-linux-gnu -S -emit-llvm gcd.c 118 | \end{tcolorbox} 119 | 120 | gcd函数现在看起来不同了。这是第一个基本块:\par 121 | 122 | \begin{tcolorbox}[colback=white,colframe=black] 123 | define i32 @gcd(i32, i32) \{ \\ 124 | \hspace*{0.5cm}\%3 = alloca i32, align 4 \\ 125 | \hspace*{0.5cm}\%4 = alloca i32, align 4 \\ 126 | \hspace*{0.5cm}\%5 = alloca i32, align 4 \\ 127 | \hspace*{0.5cm}\%6 = alloca i32, align 4 \\ 128 | \hspace*{0.5cm}store i32 \%0, i32* \%4, align 4 \\ 129 | \hspace*{0.5cm}store i32 \%1, i32* \%5, align 4 \\ 130 | \hspace*{0.5cm}\%7 = load i32, i32* \%5, align 4 \\ 131 | \hspace*{0.5cm}\%8 = icmp eq i32 \%7, 0 \\ 132 | \hspace*{0.5cm}br i1 \%8, label \%9, label \%11 133 | \end{tcolorbox} 134 | 135 | IR编码现在可以自动传递寄存器和标签的编号,为未指定参数名称。默认情况下,它们是\%0和\%1。基本块没有标签,所以赋值为2。第一个指令为4个32位值分配内存。之后,参数\%0和\%1存储在寄存器\%4和\%5所指向的内存中。要执行参数\%1与0的比较,显式地从内存加载该值(使用这种方法,而不是phi指令)!相反,可以从内存加载一个值,对其执行计算,并将新值存储回内存中。下一次读取内存时,将得到最后计算的值。gcd函数的所有其他基本块都遵循这个模式。\par 136 | 137 | 以这种方式使用加载-存储指令的好处是,生成IR代码相当容易。缺点是,在将基本块转换为SSA形式之后,将生成大量IR指令,LLVM将在第一个优化步骤中使用mem2reg通道删除这些指令。因此,我们直接以SSA的形式生成IR代码。\par 138 | 139 | 我们将控制流映射到基本块开始,来完成开发IR代码生成。\par 140 | 141 | \hspace*{\fill} \par %插入空行 142 | \textbf{将控制流映射到基本块} 143 | 144 | 如前一节所述,格式良好的基本块只是指令的线性序列。一个基本块可以从phi指令开始,必须以一个分支指令结束。基本块中,不允许使用phi和分支指令。每个基本块都有一个标签,标记基本块的第一条指令。标签是分支指令的目标。可以将分支视为两个基本块之间的有向边,从而得到控制流图(CFG)。一个基本模块可以有前身和继任者,不过函数的第一个基本块是特殊的(没有有任何块在它之前)。\par 145 | 146 | 由于这些限制,源语言的控制语句(如WHILE或IF)会生成几个基本块。让我们看看WHILE语句,WHILE语句的条件控制是执行循环体还是执行下一条语句。条件必须在它自己的基本块中生成,因为前面有两个块:\par 147 | 148 | \begin{itemize} 149 | \item 由WHILE循环之前的语句产生的基本块 150 | \item 循环体的末端条件分支 151 | \end{itemize} 152 | 153 | 还有两个后继:\par 154 | 155 | \begin{itemize} 156 | \item 循环体的开始部分 157 | \item 由WHILE循环后的语句产生的基本块 158 | \end{itemize} 159 | 160 | 循环体本身至少有一个基本块:\par 161 | 162 | \hspace*{\fill} \par %插入空行 163 | \begin{center} 164 | \includegraphics{content/2/chapter5/images/1.jpg}\\ 165 | 图5.1 – WHILE语句的基本块 166 | \end{center} 167 | 168 | IR代码生成遵循这个结构。在CGProcedure类中存储一个指向当前基本块的指针,并使用llvm::IRBuilder<>的实例,将指令插入到基本块中。首先,创建基本块:\par 169 | 170 | \begin{lstlisting}[caption={}] 171 | void emitStmt(WhileStatement *Stmt) { 172 | llvm::BasicBlock *WhileCondBB = llvm::BasicBlock::Create( 173 | getLLVMCtx(), "while.cond", Fn); 174 | llvm::BasicBlock *WhileBodyBB = llvm::BasicBlock::Create( 175 | getLLVMCtx(), "while.body", Fn); 176 | llvm::BasicBlock *AfterWhileBB = 177 | llvm::BasicBlock::Create( 178 | getLLVMCtx(), "after.while", Fn); 179 | \end{lstlisting} 180 | 181 | Fn变量表示当前函数,getLLVMCtx()返回LLVM上下文,两者都会在之后设置。我们用一个基本块的分支来结束当前的基本块,保存条件:\par 182 | 183 | \begin{lstlisting}[caption={}] 184 | Builder.CreateBr(WhileCondBB); 185 | \end{lstlisting} 186 | 187 | 条件的基本块成为新的当前基本块。我们生成条件并以条件分支结束代码块:\par 188 | 189 | \begin{lstlisting}[caption={}] 190 | setCurr(WhileCondBB); 191 | llvm::Value *Cond = emitExpr(Stmt->getCond()); 192 | Builder.CreateCondBr(Cond, WhileBodyBB, AfterWhileBB); 193 | \end{lstlisting} 194 | 195 | 接下来,生成循环体。作为最后一条指令,我们向条件的基本块添加一个分支:\par 196 | 197 | \begin{lstlisting}[caption={}] 198 | setCurr(WhileBodyBB); 199 | emit(Stmt->getWhileStmts()); 200 | Builder.CreateBr(WhileCondBB); 201 | \end{lstlisting} 202 | 203 | 这就结束了WHILE语句的生成。WHILE语句后面的空基本块将成为新的当前基本块:\par 204 | 205 | \begin{lstlisting}[caption={}] 206 | setCurr(AfterWhileBB); 207 | } 208 | \end{lstlisting} 209 | 210 | 按照这个模式,您可以为源语言的每个语句创建emit()方法。\par 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | -------------------------------------------------------------------------------- /content/2/chapter5/4.tex: -------------------------------------------------------------------------------- 1 | 2 | 3 | 我们在LLVM模块中收集编译单元的所有函数和全局变量。为了方便IR的生成,我们将前面几节中的所有函数包装在代码生成器类中。要获得一个编译器,还需要定义要为其生成代码的目标体系结构,并添加生成代码的Pass。我们将在下一章中实现所有这些,就先从代码生成器开始。\par 4 | 5 | \hspace*{\fill} \par %插入空行 6 | \textbf{包装代码生成器} 7 | 8 | IR模块是我们为编译单元生成的所有元素的大括号。在全局级别,我们遍历模块级别的声明,并创建全局变量,并为过程调用代码生成。tinylang中的全局变量会映射到llvm::GobalValue类的实例。这个映射保存在Globals中,并可用于过程代码的生成:\par 9 | 10 | \begin{lstlisting}[caption={}] 11 | void CGModule::run(ModuleDeclaration *Mod) { 12 | for (auto *Decl : Mod->getDecls()) { 13 | if (auto *Var = 14 | llvm::dyn_cast(Decl)) { 15 | llvm::GlobalVariable *V = new llvm::GlobalVariable( 16 | *M, convertType(Var->getType()), 17 | /*isConstant=*/false, 18 | llvm::GlobalValue::PrivateLinkage, nullptr, 19 | mangleName(Var)); 20 | Globals[Var] = V; 21 | } else if (auto *Proc = 22 | llvm::dyn_cast( 23 | Decl)) { 24 | CGProcedure CGP(*this); 25 | CGP.run(Proc); 26 | } 27 | } 28 | } 29 | \end{lstlisting} 30 | 31 | 该模块还持有LLVMContext类,并缓存最常用的LLVM类型。后者需要初始化,例如:对于64位整数类型:\par 32 | 33 | \begin{lstlisting}[caption={}] 34 | Int64Ty = llvm::Type::getInt64Ty(getLLVMCtx()); 35 | \end{lstlisting} 36 | 37 | CodeGenerator类初始化LLVM IR模块,并调用该模块的代码生成。最重要的是,这个类必须知道我们为哪个目标体系结构生成代码。这个信息在llvm::TargetMachine类中传递,在驱动程序中设置:\par 38 | 39 | \begin{lstlisting}[caption={}] 40 | void CodeGenerator::run(ModuleDeclaration *Mod, std::string 41 | FileName) { 42 | llvm::Module *M = new llvm::Module(FileName, Ctx); 43 | M->setTargetTriple(TM->getTargetTriple().getTriple()); 44 | M->setDataLayout(TM->createDataLayout()); 45 | CGModule CGM(M); 46 | CGM.run(Mod); 47 | } 48 | \end{lstlisting} 49 | 50 | 为了方便使用,还为代码生成器引入了工厂方法:\par 51 | 52 | \begin{lstlisting}[caption={}] 53 | CodeGenerator *CodeGenerator::create(llvm::TargetMachine *TM) { 54 | return new CodeGenerator(TM); 55 | } 56 | \end{lstlisting} 57 | 58 | CodeGenerator类提供了创建IR代码的接口,非常适合在编译器驱动程序中使用。在集成之前,需要支持实现对机器码的生成。\par 59 | 60 | \hspace*{\fill} \par %插入空行 61 | \textbf{初始化目标机器类} 62 | 63 | 现在,只缺少创建目标机器类。在目标机器上,我们定义了用于生成代码的CPU体系结构。对于每个CPU,还可以使用一些可用的特性来影响代码生成,例如:CPU体系结构家族中较新的CPU可以支持向量指令。通过特性,我们可以打开或关闭向量指令的使用。为了支持从命令行设置所有这些选项,LLVM提供了一些支持代码。在Driver类中,添加了以下include变量:\par 64 | 65 | \begin{lstlisting}[caption={}] 66 | #include "llvm/CodeGen/CommandFlags.h" 67 | \end{lstlisting} 68 | 69 | 这个include变量为编译器驱动程序添加了常用的命令行选项。许多LLVM工具也使用这些命令行选项,好处是为用户提供了一个公共界面(只缺少指定目标三元组的选项)。由于这非常好用,所以我们添加了这个:\par 70 | 71 | \begin{lstlisting}[caption={}] 72 | static cl::opt 73 | MTriple("mtriple", 74 | cl::desc("Override target triple for module")); 75 | \end{lstlisting} 76 | 77 | 创建目标机器码:\par 78 | 79 | \begin{itemize} 80 | \item 为了显示错误消息,应用程序的名称必须传递给函数: 81 | \begin{lstlisting}[caption={}] 82 | llvm::TargetMachine * 83 | createTargetMachine(const char *Argv0) { 84 | \end{lstlisting} 85 | 86 | \item 收集命令行提供的所有信息。以下是代码生成器的选项、CPU的名称、应该开启(或关闭)的可能特性以及目标的“三元组”: 87 | \begin{lstlisting}[caption={}] 88 | llvm::Triple = llvm::Triple( 89 | !MTriple.empty() 90 | ? llvm::Triple::normalize(MTriple) 91 | : llvm::sys::getDefaultTargetTriple()); 92 | 93 | llvm::TargetOptions = 94 | codegen::InitTargetOptionsFromCodeGenFlags(Triple); 95 | std::string CPUStr = codegen::getCPUStr(); 96 | std::string FeatureStr = codegen::getFeaturesStr(); 97 | \end{lstlisting} 98 | 99 | \item 在目标注册表中查找目标。如果发生错误,则显示错误消息并退出。一个可能的错误是,用户指定的不支持的三元组: 100 | \begin{lstlisting}[caption={}] 101 | std::string Error; 102 | const llvm::Target *Target = 103 | llvm::TargetRegistry::lookupTarget( 104 | codegen::getMArch(), Triple, 105 | Error); 106 | 107 | if (!Target) { 108 | llvm::WithColor::error(llvm::errs(), Argv0) << 109 | Error; 110 | return nullptr; 111 | } 112 | \end{lstlisting} 113 | 114 | \item 在Target类的帮助下,使用用户请求的所有已知选项配置目标机器: 115 | \begin{lstlisting}[caption={}] 116 | llvm::TargetMachine *TM = Target-> 117 | createTargetMachine( 118 | Triple.getTriple(), CPUStr, FeatureStr, 119 | TargetOptions, 120 | llvm::Optional( 121 | codegen::getRelocModel())); 122 | return TM; 123 | } 124 | \end{lstlisting} 125 | 126 | \end{itemize} 127 | 128 | 通过目标机器实例,可以生成针对我们选择的CPU架构的IR代码。缺少的是对程序集文本的转换或目标代码文件的生成。我们将在下一节中添加这种支持。\par 129 | 130 | \hspace*{\fill} \par %插入空行 131 | \textbf{生成汇编程序文本和目标代码} 132 | 133 | 在LLVM中,IR代码通过Pass运行。每一遍执行一个任务,例如:删除死代码。我们将在第8章,优化IR中学习更多关于Pass的内容。输出汇编程序代码或目标文件也可以实现为Pass。\par 134 | 135 | 需要llvm::legacy::PassManager类来保存Pass,以便将代码发送到文件。还希望能够输出LLVM IR代码,因此也需要一个Pass来做这个。最后,使用llvm:: ToolOutputFile类进行文件操作:\par 136 | 137 | \begin{lstlisting}[caption={}] 138 | #include "llvm/IR/IRPrintingPasses.h" 139 | #include "llvm/IR/LegacyPassManager.h" 140 | #include "llvm/Support/ToolOutputFile.h" 141 | \end{lstlisting} 142 | 143 | 输出LLVM IR的另一个命令行选项也是必需的:\par 144 | 145 | \begin{lstlisting}[caption={}] 146 | static cl::opt 147 | EmitLLVM("emit-llvm", 148 | cl::desc("Emit IR code instead of assembler"), 149 | cl::init(false)); 150 | \end{lstlisting} 151 | 152 | 新的emit()方法中的第一个任务是处理输出文件的名称。如果从stdin读取输入(用减号-表示),则将结果输出到stdout。ToolOutputFile类知道如何处理特殊文件名,-:\par 153 | 154 | \begin{lstlisting}[caption={}] 155 | bool emit(StringRef Argv0, llvm::Module *M, 156 | llvm::TargetMachine *TM, 157 | StringRef InputFilename) { 158 | CodeGenFileType FileType = codegen::getFileType(); 159 | std::string OutputFilename; 160 | if (InputFilename == "-") { 161 | OutputFilename = "-"; 162 | } 163 | \end{lstlisting} 164 | 165 | 否则,根据用户给出的命令行选项,我们将删除输入文件名的可能扩展名,并附加.ll、.s或.o作为扩展名。FileType选项在llvm/CodeGen/CommandFlags.inc头文件中定义,我们之前包含了它。这个选项不支持发出IR代码,所以我们添加了新的选项-emit-llvm,只有在与汇编文件一起使用时才会生效:\par 166 | 167 | \begin{lstlisting}[caption={}] 168 | else { 169 | if (InputFilename.endswith(".mod")) 170 | OutputFilename = InputFilename.drop_back(4).str(); 171 | else 172 | OutputFilename = InputFilename.str(); 173 | switch (FileType) { 174 | case CGFT_AssemblyFile: 175 | OutputFilename.append(EmitLLVM ? ".ll" : ".s"); 176 | break; 177 | case CGFT_ObjectFile: 178 | OutputFilename.append(".o"); 179 | break; 180 | case CGFT_Null: 181 | OutputFilename.append(".null"); 182 | break; 183 | } 184 | } 185 | \end{lstlisting} 186 | 187 | 有些平台区分文本文件和二进制文件,所以必须在打开输出文件时提供正确的标志:\par 188 | 189 | \begin{lstlisting}[caption={}] 190 | std::error_code EC; 191 | sys::fs::OpenFlags = sys::fs::OF_None; 192 | if (FileType == CGFT_AssemblyFile) 193 | OpenFlags |= sys::fs::OF_Text; 194 | auto Out = std::make_unique( 195 | OutputFilename, EC, OpenFlags); 196 | if (EC) { 197 | WithColor::error(errs(), Argv0) << EC.message() << 198 | '\n'; 199 | return false; 200 | } 201 | \end{lstlisting} 202 | 203 | 现在我们可以将所需的Pass添加到PassManager中。TargetMachine类有一个实用程序方法,用于添加请求的类。因此,我们只需要检查用户是否请求输出LLVM IR代码:\par 204 | 205 | \begin{lstlisting}[caption={}] 206 | legacy::PassManager PM; 207 | if (FileType == CGFT_AssemblyFile && EmitLLVM) { 208 | PM.add(createPrintModulePass(Out->os())); 209 | } else { 210 | if (TM->addPassesToEmitFile(PM, Out->os(), nullptr, 211 | FileType)) { 212 | WithColor::error() << "No support for file type\n"; 213 | return false; 214 | } 215 | } 216 | \end{lstlisting} 217 | 218 | 所有这些准备工作完成后,生成文件就可以归结为一个函数调用:\par 219 | 220 | \begin{lstlisting}[caption={}] 221 | PM.run(*M); 222 | \end{lstlisting} 223 | 224 | 如果不显式地保留该文件,那么ToolOutputFile类会自动删除该文件。这使得错误处理更容易,因为可能有很多地方需要处理错误,而在一切顺利的情况下只需要到达相应的期望。这里,我们成功地生成了代码,所以想要保留这个文件:\par 225 | 226 | \begin{lstlisting}[caption={}] 227 | Out->keep(); 228 | \end{lstlisting} 229 | 230 | 最后,我们向调用者报告成功:\par 231 | 232 | \begin{lstlisting}[caption={}] 233 | return true; 234 | } 235 | \end{lstlisting} 236 | 237 | 使用llvm::Module调用emit()方法,并调用CodeGenerator类,将按照请求生成代码。\par 238 | 239 | 假设在tinylang中有最大公约数算法存储在gcd.mod文件中。把它翻译成gcd.os目标文件,需要输入以下内容:\par 240 | 241 | \begin{tcolorbox}[colback=white,colframe=black] 242 | \$ tinylang –filetype=obj gcd.mod 243 | \end{tcolorbox} 244 | 245 | 如果想在屏幕上直接检查生成的IR代码,可以输入以下代码:\par 246 | 247 | \begin{tcolorbox}[colback=white,colframe=black] 248 | \$ tinylang –filetype=asm –emit-llvm –o – gcd.mod 249 | \end{tcolorbox} 250 | 251 | 让我们好好庆祝一下吧!至此,已经创建了一个完整的编译器,从读取源语言到生成汇编代码或目标文件。\par 252 | 253 | 254 | 255 | 256 | -------------------------------------------------------------------------------- /content/2/chapter5/5.tex: -------------------------------------------------------------------------------- 1 | 本章中,学习了如何为LLVM IR代码实现你自己的代码生成器。基本块是一种重要的数据结构,包含所有的指令并表示分支。您学习了如何为源语言的控制语句创建基本块,以及如何向基本块添加指令。您应用了一种现代算法来处理函数中的局部变量,从而减少了IR代码。编译器的目标是为输入生成汇编文本或目标文件,因此还添加了一个简单的编译Pass。有了这些知识,您将能够生成LLVM IR,并随后为您自己的语言编译器生成汇编文本或目标代码。\par 2 | 3 | 下一章中,您将学习如何处理聚合数据结构,以及如何确保函数调用符合规则。\par 4 | 5 | \newpage -------------------------------------------------------------------------------- /content/2/chapter5/images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/2/chapter5/images/1.jpg -------------------------------------------------------------------------------- /content/2/chapter6/0.tex: -------------------------------------------------------------------------------- 1 | 目前,高级语言通常使用聚合数据类型和面向对象编程(OOP)构造。LLVM IR对聚合数据类型有一定的支持,必须自己实现OOP构造。添加聚合类型会引起如何传递聚合类型参数的问题。不同的平台有不同的规则,这也体现在IR中。遵循调用约定可以确保系统函数可以调用。\par 2 | 3 | 本章中,您将学习如何转换聚合数据类型和指向LLVM IR的指针,以及如何以系统兼容的方式将参数传递给函数。您还将学习如何在LLVM IR中实现类和虚函数。\par 4 | 5 | 本章将包含以下内容:\par 6 | 7 | \begin{itemize} 8 | \item 使用数组、结构和指针 9 | \item 获取正确的二进制接口 10 | \item 为类和虚函数创建IR代码 11 | \end{itemize} 12 | 13 | 在本章结束时,您将为聚合数据类型和OOP创建LLVM IR。您还会了解如何根据平台规则传递聚合数据类型。\par 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /content/2/chapter6/1.tex: -------------------------------------------------------------------------------- 1 | 本章的代码文件可在\url{https://github.com/PacktPublishing/Learn-LLVM-12/tree/master/Chapter06/tinylang}获取。\par 2 | 3 | 你可以在视频中找到代码\url{https://bit.ly/3nllhED}。\par -------------------------------------------------------------------------------- /content/2/chapter6/2.tex: -------------------------------------------------------------------------------- 1 | 对于所有的应用程序,基本类型(如INTEGER)是不够用的。例如,要表示矩阵或复数等数学对象,必须基于现有数据类型构造新的数据类型。这些新的数据类型通常称为聚合或复合类型。\par 2 | 3 | 数组是同一类型元素的序列。在LLVM中,数组总是静态的:元素的数量是常量。整数数组[10]的tinylang类型,或长整数数组[10]的C类型,可以用IR表示为:\par 4 | 5 | \begin{tcolorbox}[colback=white,colframe=black] 6 | [10 x i64] 7 | \end{tcolorbox} 8 | 9 | 结构是不同类型的复合。在编程语言中,通常用命名成员来表示,例如:在tinylang中,结构写为RECORD x, y: REAL; color: INTEGER; END;同样的结构在C中是struct \{float x, y;long color;\};。在LLVM IR中,只列出了类型名:\par 10 | 11 | \begin{tcolorbox}[colback=white,colframe=black] 12 | \{ float, float, i64 \} 13 | \end{tcolorbox} 14 | 15 | 要访问成员,需要使用数字索引。与数组一样,第一个元素的索引号是0。\par 16 | 17 | 此结构的成员根据数据布局字符串中的规范在内存中进行布局。如果有必要,将插入未使用的填充字节。如果需要控制内存布局,可以使用打包结构,其中所有元素都是1字节对齐的。语法略有不同:\par 18 | 19 | \begin{tcolorbox}[colback=white,colframe=black] 20 | <{ float, float, i64 }> 21 | \end{tcolorbox} 22 | 23 | 数组和结构可以作为一个单元来处理,例如:不能将\%x数组的单个元素以\%x[3]引用。这时因为SSA的形式不能分辨\%x[i]和\%x[j]是否指相同的元素。相反,需要特殊的指令来提取和插入单个元素的值到数组中。要读取第二个元素,可以使用以下语句:\par 24 | 25 | \begin{tcolorbox}[colback=white,colframe=black] 26 | \%el2 = extractvalue [10 x i64] \%x, 1 27 | \end{tcolorbox} 28 | 29 | 我们也可以更新一个元素,例如:第一个元素:\par 30 | 31 | \begin{tcolorbox}[colback=white,colframe=black] 32 | \%xnew = insertvalue [10 x i64] \%x, i64 \%el2, 0 33 | \end{tcolorbox} 34 | 35 | 这两种指令也适用于结构体。例如,要从\%pt寄存器访问color成员,需要编写以下代码:\par 36 | 37 | \begin{tcolorbox}[colback=white,colframe=black] 38 | \%color = extractvalue { float, float, i64 } \%pt, 2 39 | \end{tcolorbox} 40 | 41 | 这两个指令都有一个限制:索引必须是常数。对于结构来说,这很容易解释。索引号只是名称的替代,而C等语言没有动态计算结构成员名称的概念。对于数组,只是不能有效地实现。这两种指令在特定的情况下都有意义,当元素的数量很小且已知时,例如:复数可以建模为两个浮点数的数组。传递这个数组是合理的,并且在计算过程中访问数组的哪一部分是很清楚的。\par 42 | 43 | 对于前端的一般使用,需要求助于指向内存的指针。LLVM中的所有全局值都表示为指针,声明一个全局变量@arr,作为一个包含8个i64元素的数组,等价于long arr[8]的C声明:\par 44 | 45 | \begin{tcolorbox}[colback=white,colframe=black] 46 | @arr = common global [8 x i64] zeroinitializer 47 | \end{tcolorbox} 48 | 49 | 要访问数组的第二个元素,必须执行地址计算,以确定索引的地址。然后,可以从该地址加载该值。将其放入@second函数中:\par 50 | 51 | \begin{tcolorbox}[colback=white,colframe=black] 52 | define i64 @second() { \\ 53 | \hspace*{0.5cm}\%1 = getelementptr [8 x i64], [8 x i64]* @arr, i64 0, i64 \\ 54 | \hspace*{0.5cm}1 55 | \hspace*{0.5cm}\%2 = load i64, i64* \%1 \\ 56 | \hspace*{0.5cm}ret i64 \%2 \\ 57 | } 58 | \end{tcolorbox} 59 | 60 | getelementptr指令是地址计算的主要工具,所以需要对其进行更多的介绍。第一个操作数[8 x i64]是该指令操作的基类型。第二个操作数[8 x i64]* @arr指定基指针。注意这里的细微差别:我们声明了一个包含8个元素的数组,而所有全局值都视为指针,所以有一个指向数组的指针。C语法中,我们使用long (*arr)[8]!其结果是,在对元素进行索引之前,首先必须对指针进行解引用,例如:C中的arr[0][1]。第三个操作数i64 0对指针进行解引用,第四个操作数i64 1是元素的下标。计算的结果是索引元素的地址。请注意,本指令没有涉及内存。\par 61 | 62 | 除了结构之外,索引参数不需要是常量。因此,可以在循环中使用getelementptr指令来检索数组的元素。这里对结构的处理是不同的:只能使用常量,类型必须是i32。\par 63 | 64 | 有了这些知识,数组很容易集成到第5章的代码生成器中。必须扩展convertType()方法来创建类型,如果Arr变量持有数组的类型指示符,那么可以在方法中添加以下内容:\par 65 | 66 | \begin{lstlisting}[caption={}] 67 | llvm::Type *Component = convertType(Arr->getComponentType()); 68 | uint64_t NumElements = Arr->getNumElem(); 69 | return llvm::ArrayType::get(Component, NumElements); 70 | \end{lstlisting} 71 | 72 | 该类型可用于声明全局变量。对于局部变量,需要为数组分配内存。在过程的第一个基本块中可以完成这个操作:\par 73 | 74 | \begin{lstlisting}[caption={}] 75 | for (auto *D : Proc->getDecls()) { 76 | if (auto *Var = 77 | llvm::dyn_cast(D)) { 78 | llvm::Type *Ty = mapType(Var); 79 | if (Ty->isAggregateType()) { 80 | llvm::Value *Val = Builder.CreateAlloca(Ty); 81 | Defs.Defs.insert( 82 | std::pair(Var, Val)); 83 | } 84 | } 85 | } 86 | \end{lstlisting} 87 | 88 | 要读写一个元素,必须生成getelemtptr指令。这个指令会添加到emitExpr()(读取值)和emitAssign()(写入值)的方法中。要读取数组元素,首先读取变量的值,然后处理变量的选择器。对于每个索引,计算表达式并存储其值。基于这个列表,计算引用元素的地址并加载值:\par 89 | 90 | \begin{lstlisting}[caption={}] 91 | auto &Selectors = Var->getSelectorList(); 92 | for (auto *I = Selectors.begin(), 93 | *E = Selectors.end(); 94 | I != E;) { 95 | if (auto *Idx = llvm::dyn_cast(*I)) { 96 | llvm::SmallVector IdxList; 97 | IdxList.push_back(emitExpr(Idx->getIndex())); 98 | for (++I; I != E;) { 99 | if (auto *Idx2 = 100 | llvm::dyn_cast(*I)) { 101 | IdxList.push_back(emitExpr(Idx2->getIndex())); 102 | ++I; 103 | } else 104 | break; 105 | } 106 | Val = Builder.CreateGEP(Val, IdxList); 107 | Val = Builder.CreateLoad( 108 | Val->getType()->getPointerElementType(), Val); 109 | } else { 110 | llvm::report_fatal_error("Unsupported selector"); 111 | } 112 | } 113 | \end{lstlisting} 114 | 115 | 写入数组元素使用相同的代码,但不生成加载指令。相反,在存储指令中使用指针作为目标。对于记录,可以使用类似的方法。记录成员的选择器包含常量字段索引,名为Idx。可以通过以下方法将这个常量转换为LLVM常量值:\par 116 | 117 | \begin{lstlisting}[caption={}] 118 | llvm::Value *FieldIdx = llvm::ConstantInt::get(Int32Ty, Idx); 119 | \end{lstlisting} 120 | 121 | 然后,您可以像使用数组一样使用Builder.CreateGEP()方法中的值。\par 122 | 123 | 现在您已经掌握了将聚合数据类型转换为LLVM IR的方法。以系统兼容的方式传递这些类型的值需要注意一些细节,在下一节中您将学习如何正确地实现它。\par 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /content/2/chapter6/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 使用最新添加的数组和记录到代码生成器中,您可能会注意到有时生成的代码不能按预期执行。原因是目前为止我们忽略了平台的调用规则。对于同一个程序或库中的一个函数如何调用另一个函数,每个平台都定义了自己的规则。这些规则在应用程序二进制接口(ABI)文档中进行了总结。通常的信息包括:\par 3 | 4 | \begin{itemize} 5 | \item 是否使用机器寄存器传递参数?如果是,是哪个? 6 | \item 如何将数组和结构体等聚合体传递给函数? 7 | \item 如何处理返回值? 8 | \end{itemize} 9 | 10 | 在使用中有各种各样的规则时,在一些平台上,聚合体总是间接传递,是将聚合体的副本放在堆栈上,只将指向该副本的指针作为参数传递。在其他平台上,在寄存器中传递小的聚合体(比如128位或256位宽),只有超个阈值才使用间接参数传递。有些平台还使用浮点和向量寄存器传递参数,而有些平台要求浮点值在整数寄存器中传递。\par 11 | 12 | 当然,这些都是很有趣的、很底层的东西。不幸的是,它与LLVM IR相关。这还挺令人惊讶的,毕竟我们在LLVM IR中定义了函数的所有参数的类型!事实证明,这还不够。为了理解这一点,来考虑下复数。有些语言有用于复数的内置数据类型,例如:C99有float\underline{~}Complex(等等)。旧版本的C没有复数类型,但可以很容易地定义struct complex \{float re, im;\}并在该类型上创建算术运算。这两种类型都可以映射到\{float, float \} LLVM IR类型。如果ABI现在声明内置复数类型的值在两个浮点寄存器中传递,但用户定义的聚合体总是间接传递,那么该函数提供的信息不足以让LLVM决定如何传递这个特定参数。不幸的是,我们需要向LLVM提供更多信息,而这些是高度特定于ABI的信息。\par 13 | 14 | 有两种方法可以将此信息指定给LLVM:参数属性和类型重写。您需要使用的内容取决于目标平台和代码生成器,常用的参数属性如下:\par 15 | 16 | \begin{itemize} 17 | \item inreg指定参数在寄存器中传递。 18 | \item byval指定按值传递参数。形参必须是指针类型。指向数据的隐藏副本,并传递给调用的函数。 19 | \item zeroext和signext指定传递的整数值应为零或扩展符号。 20 | \item sret此形参保存一个指向内存的指针,该指针用于从函数返回聚合体类型。 21 | \end{itemize} 22 | 23 | 虽然所有代码生成器都支持zeroext、signext和sret属性,但只有部分代码生成器支持inreg和byval。可以使用addAttr()方法将属性添加到函数的参数中,例如:要在Arg参数上设置inreg属性,可以调用以下方法:\par 24 | 25 | \begin{lstlisting}[caption={}] 26 | Arg->addAttr(llvm::Attribute::InReg); 27 | \end{lstlisting} 28 | 29 | 要设置多个属性,可以使用llvm::AttrBuilder类。\par 30 | 31 | 提供附加信息的另一种方法是使用类型重写。使用这种方法,可以隐藏原始类型:\par 32 | 33 | \begin{itemize} 34 | \item 拆分参数,例如:可以传递两个浮点参数,而不是传递一个复杂参数。 35 | \item 将参数转换为不同的表示形式,例如:将大小为64位或更小的结构体转换为i64整数。 36 | \end{itemize} 37 | 38 | 在不改变值位数的情况下,在不同类型之间进行转换,可以使用位转换指令。bitcast指令不会对聚合类型进行操作,但这不是一个限制,因为您可以使用指针。如果一个聚合体为一个具有两个int成员的结构体,在LLVM中表示为类型\{i32, i32 \},那么这个聚合体可以按以下方式位转换为i64:\par 39 | 40 | \begin{tcolorbox}[colback=white,colframe=black] 41 | \%intpoint = bitcast { i32, i32}* \%point to i64* 42 | \end{tcolorbox} 43 | 44 | 这将把指向结构体的指针转换为指向i64整数值的指针。随后,可以加载该值并将其作为参数传递。必须确保这两种类型具有相同的大小。\par 45 | 46 | 向参数添加属性或更改类型并不复杂。但是,如何知道需要实现什么呢?首先,应该大致了解目标平台上使用的调用约定,例如:Linux上的ELF ABI记录了每种支持的CPU平台(只要只需要查一下文件)。有关于LLVM代码生成器需求的文档,信息的来源是Clang实现,在\url{https://github.com/llvm/llvm-project/blob/main/clang/lib/CodeGen/TargetInfo.cpp}文件中。这个文件包含所有受支持平台的特定于ABI的操作。\par 47 | 48 | 本节中,您学习了如何为函数调用生成符合平台ABI的IR。下一节将介绍为类和虚函数创建IR的不同方法。\par 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /content/2/chapter6/4.tex: -------------------------------------------------------------------------------- 1 | 许多现代编程语言都支持使用类面向对象方式。类是一种高级语言构造,在本节中,我们将探讨如何将类构造映射到LLVM IR中。\par 2 | 3 | \hspace*{\fill} \par %插入空行 4 | \textbf{实现单继承} 5 | 6 | 类是数据和方法的集合。一个类可以从另一个类继承,可能会添加更多的数据字段和方法,或者覆盖现有的虚方法。我们用Oberon-2中的类来说明这一点,它也是tinylang的一个模型。Shape类定义了一个带有颜色和区域的抽象形状:\par 7 | 8 | \begin{tcolorbox}[colback=white,colframe=black] 9 | TYPE Shape = RECORD \\ 10 | \hspace*{3cm}color: INTEGER; \\ 11 | \hspace*{3cm}PROCEDURE (VAR s: Shape) GetColor(): \\ 12 | \hspace*{3.5cm}INTEGER; \\ 13 | \hspace*{3cm}PROCEDURE (VAR s: Shape) Area(): REAL;\\ 14 | \hspace*{2.5cm}END; 15 | \end{tcolorbox} 16 | 17 | GetColor方法只返回颜色编号:\par 18 | 19 | \begin{tcolorbox}[colback=white,colframe=black] 20 | PROCEDURE (VAR s: Shape) GetColor(): INTEGER; \\ 21 | BEGIN RETURN s.color; END GetColor; 22 | \end{tcolorbox} 23 | 24 | 抽象的形状的面积是无法计算的,所以这是一个抽象方法:\par 25 | 26 | \begin{tcolorbox}[colback=white,colframe=black] 27 | PROCEDURE (VAR s: Shape) Area(): REAL; \\ 28 | BEGIN HALT; END; 29 | \end{tcolorbox} 30 | 31 | Shape类型可以扩展为表示Circle类:\par 32 | 33 | \begin{tcolorbox}[colback=white,colframe=black] 34 | TYPE Circle = RECORD (Shape) \\ 35 | \hspace*{3cm}radius: REAL; \\ 36 | \hspace*{3cm}PROCEDURE (VAR s: Circle) Area(): REAL; \\ 37 | \hspace*{2.5cm}END; 38 | \end{tcolorbox} 39 | 40 | 对于圆,面积公式为:\par 41 | 42 | \begin{tcolorbox}[colback=white,colframe=black] 43 | PROCEDURE (VAR s: Circle) Area(): REAL; \\ 44 | BEGIN RETURN 2 * radius * radius; END; 45 | \end{tcolorbox} 46 | 47 | 还可以在运行时查询该类型。如果shape是Shape类型的变量,则可以这样制定类型测试:\par 48 | 49 | \begin{tcolorbox}[colback=white,colframe=black] 50 | IF shape IS Circle THEN (* … *) END; 51 | \end{tcolorbox} 52 | 53 | 除了语法不同之外,工作原理与C++非常相似。与C++的明显区别是,Oberon-2语法使隐式的this指针显式化,将其称为接收方方法。\par 54 | 55 | 需要解决的基本问题是如何在内存中布局类,以及如何实现方法的动态调用和运行时类型检查。对于内存布局,这是相当容易的。Shape类只有一个数据成员,可以将它映射到相应的LLVM结构类型:\par 56 | 57 | \begin{tcolorbox}[colback=white,colframe=black] 58 | @Shape = type \{ i64 \} 59 | \end{tcolorbox} 60 | 61 | Circle类添加了另一个数据成员,直接在末尾添加新的数据成员:\par 62 | 63 | \begin{tcolorbox}[colback=white,colframe=black] 64 | @Circle = type \{ i64, float \} 65 | \end{tcolorbox} 66 | 67 | 原因是一个类可以有很多子类。使用这种策略,公共基类的数据成员总是具有相同的内存偏移量,并使用相同的索引通过getelementptr指令访问字段。\par 68 | 69 | 为了实现方法的动态调用,必须进一步扩展LLVM结构。如果在Shape对象上调用Area()函数,则会调用抽象方法,导致应用程序停止。如果在Circle对象上调用该函数,则调用相应的方法来计算圆的面积。这两个类的对象都可以调用GetColor()函数。实现这个的基本思想是将表与函数指针与每个对象关联起来。这里,该表有两个条目:一个用于GetColor()方法,一个用于Area()函数。Shape类和Circle类都有这样一个表。这两个表在Area()函数的条目上有所不同,该函数根据对象的类型调用不同的代码。这个表称为虚方法表,通常缩写为vtable。\par 70 | 71 | 单独使用vtable是没有用的,我们必须把它和一个物体联系起来。为此,可以添加一个指向虚函数表的指针,该指针总是作为结构的第一个数据成员。在LLVM层,@Shape类型变成如下表示:\par 72 | 73 | \begin{tcolorbox}[colback=white,colframe=black] 74 | @Shape = type \{ [2 x i8*]*, i64 \} 75 | \end{tcolorbox} 76 | 77 | @Circle类型的扩展也是类似的,得到的内存结构如图6.1所示:\par 78 | 79 | \hspace*{\fill} \par %插入空行 80 | \begin{center} 81 | \includegraphics{content/2/chapter6/images/1.jpg}\\ 82 | 图6.1 – 类和虚拟方法表的内存布局 83 | \end{center} 84 | 85 | LLVM没有空指针,而是使用指向字节的指针。引入了隐藏的虚函数表字段后,现在还需要一种方法来初始化它。在C++中,这是构造函数的一部分。在Oberon-2中,该字段在分配内存时自动初始化。\par 86 | 87 | 然后按以下步骤执行对方法的动态调用:\par 88 | 89 | \begin{enumerate} 90 | \item 通过getelementptr指令计算虚表指针的偏移量。 91 | \item 加载指向虚函数表的指针。 92 | \item 计算虚函数表中函数的偏移量。 93 | \item 加载函数指针。 94 | \item 通过带有调用指令的指针间接调用函数。 95 | \end{enumerate} 96 | 97 | 这听起来不是很高效,但事实上,大多数CPU架构只需要两条指令就可以执行这个动态调用。因此,真正低效的是LLVM层。\par 98 | 99 | 要将函数转换为方法,需要对对象数据的引用。这是通过将指向数据的指针,作为函数的第一个参数来实现的。在Oberon-2中,这就是显式接收器。类似于C++的语言中的this指针。\par 100 | 101 | 有了虚函数表,每个类在内存中都有唯一的地址。这对运行时类型测试有帮助吗?的确有帮助,但帮助有限。为了说明这个问题,我们用继承自Circle类的Ellipse类来扩展类层次结构(这不是数学意义上的is-a关系)。如果有Shape类型的shape变量,那么可以实现shape IS Circle的类型测试,将shape变量中存储的虚函数表指针与Circle类中的虚函数表指针进行比较。只有当shape具有精确的Circle类型时,这个比较才会得到true。但是如果shape是Ellipse类型的,那么将返回false(即使Ellipse类型的对象,可以在只需要Circle类型的对象的地方使用)。\par 102 | 103 | 显然,我们需要做得更多。解决方案是使用运行时类型信息扩展虚拟函数表,需要存储多少信息取决于源语言。为了支持运行时类型检查,只要存储一个指向基类的虚函数表的指针就足够了,如图6.2所示:\par 104 | 105 | \hspace*{\fill} \par %插入空行 106 | \begin{center} 107 | \includegraphics{content/2/chapter6/images/2.jpg}\\ 108 | 图6.2 – 支持简单类型测试的类和虚函数表布局 109 | \end{center} 110 | 111 | 若测试像前面描述的那样失败,则使用指向基类的虚函数表的指针重复测试。一直重复,直到测试结果为true,如果没有基类,则为false。与调用动态函数相比,类型测试是一种开销很大的操作,因为在最坏的情况下,继承层次结构会逐步上升到根类。\par 112 | 113 | 如果您知道整个类层次结构,那么就可能有一种有效的方法:按照深度优先的顺序为类层次结构的每个成员编号。然后,类型测试就变成了对一个数字或一个区间的比较,可以在固定时间内完成。事实上,这就是LLVM自己的运行时类型测试的方法,我们在前一章已经学习过了。\par 114 | 115 | 将运行时类型信息与虚函数表耦合是一个设计上的决策,要么由源语言强制执行,要么只是一个实现。如果您需要详细的运行时类型信息,因为源语言支持运行时反射,而您的数据类型没有虚函数表,那么耦合两者就不是一个好主意。在C++中,这种耦合出现在一种情况下,即带有虚函数(因此没有虚函数表)的类没有附加运行时类型数据。\par 116 | 117 | 编程语言通常支持接口,接口是虚拟方法的集合。接口很重要,它们添加了有用的抽象。我们将在下一节中讨论接口的可能实现。\par 118 | 119 | \hspace*{\fill} \par %插入空行 120 | \textbf{使用接口扩展单继承} 121 | 122 | 像Java这样的语言支持接口。接口是抽象方法的集合,类似于没有数据成员且只定义抽象方法的基类。接口带来了一个有趣的问题,因为每个实现接口的类在虚函数表的不同位置都有相应的方法。原因很简单,虚函数表中函数指针的顺序派生自源语言中类定义中函数的顺序。接口中的定义与此无关,不同的顺序才是重点。\par 123 | 124 | 因为在接口中定义的方法可以有不同的顺序,所以将每个实现的接口的表附加到类中。对于接口的每个方法,该表既可以指定方法在虚表中的索引,也可以指定存储在虚表中的函数指针的副本。如果在接口上调用方法,则搜索接口对应的虚函数表,然后获取函数的指针并调用方法。将两个接口I1和I2添加到Shape类中会得到如下布局:\par 125 | 126 | \hspace*{\fill} \par %插入空行 127 | \begin{center} 128 | \includegraphics{content/2/chapter6/images/3.jpg}\\ 129 | 图6.3 – 接口的虚函数表布局 130 | \end{center} 131 | 132 | 需要注意的是,我们必须找到正确的虚变量表。可以使用类似于运行时类型测试的方法:可以通过接口虚函数表列表执行线性搜索。使用这个数字标识虚函数表,可以为每个接口分配一个唯一的数字(例如,内存地址)。这种模式的缺点很明显:通过接口调用方法要比在类上调用相同的方法花费更多的时间。要解决这个问题并不容易。\par 133 | 134 | 一个好的方法是用哈希表代替线性搜索。在编译时,类实现的接口已知。因此,可以构造一个哈希函数,它将接口编号映射到接口的虚函数表。在构造过程中可能需要一个已知标识接口的数字,因此内存没有帮助。但还有其他计算唯一数字的方法。如果源中的符号名是唯一的,那么可以计算加密哈希值,如符号的MD5,并使用哈希值作为数字。计算发生在编译时,因此没有运行时成本。\par 135 | 136 | 结果比线性搜索快得多,只需要常数时间。尽管如此,仍然涉及对数字的几个算术运算,比类类型的方法调用要慢。\par 137 | 138 | 通常,接口也会参与运行时类型测试,这使得需要搜索的列表更长。当然,如果实现了哈希表方法,那么它也可以用于运行时类型测试。\par 139 | 140 | 有些语言允许有多个父类。这对实现有一些挑战,我们将在下一节学习。\par 141 | 142 | \hspace*{\fill} \par %插入空行 143 | \textbf{对多重继承的支持} 144 | 145 | 多重继承增加了另一个挑战。如果一个类继承了两个或更多的基类,那么我们需要组合数据成员,使它们仍然可以从函数中访问。与单继承的情况一样,解决方案是附加所有数据成员,包括隐藏的虚函数表指针。Circle类不仅是一个几何形状,也是一个图形对象。为了对此进行建模,让Circle类继承Shape类和GraphicObj类。在类布局中,来自Shape类的字段最先出现。然后,附加GraphicObj类的所有字段,包括隐藏的虚表指针。之后,添加了Circle类的新数据成员,得到了如图6.4所示的整体结构:\par 146 | 147 | \hspace*{\fill} \par %插入空行 148 | \begin{center} 149 | \includegraphics{content/2/chapter6/images/4.jpg}\\ 150 | 图6.4 – 具有多重继承的类和虚函数表 151 | \end{center} 152 | 153 | 这种函数有几个含义。现在可以有几个指向该对象的指针。指向Shape或Circle类的指针指向对象的顶部,而指向GraphicObj类的指针指向对象内部,指向嵌入的GraphicObj对象的开头。在比较指针时必须考虑这一点。\par 154 | 155 | 调用虚函数也会受到影响。如果函数是在GraphicObj类中定义的,那么这个函数需要GraphicObj类的类布局。如果这个函数没有在Circle类中重写,那么有两种可能。简单的情况是,如果函数调用是通过一个指向GraphicObj实例的指针完成的:在GraphicObj类的虚表中查找方法的地址,然后调用函数。更复杂的情况是使用指向Circle类的指针调用函数。同样,可以在Circle类的虚表中查找函数的地址。调用的函数需要一个指向GraphicObj类实例的this指针,因此我们也必须调整该指针。我们可以这样做,因为已知GraphicObj类在Circle类中的偏移量。\par 156 | 157 | 如果在Circle类中重写了GrapicObj的函数,通过指向Circle类的指针调用该方法,则不需要做任何特殊操作。但是,如果该方法是通过指向GraphicObj实例的指针调用的,就需要进行另一个调整,因为该方法需要一个this指针指向Circle实例。编译时,我们无法计算这个调整,因为不知道这个GraphicObj实例是否是多重继承层次结构的一部分。为了解决这个问题,我们将在调用函数之前对this指针所做的调整与虚函数表中的每个函数指针一起存储起来,如图6.5所示:\par 158 | 159 | \hspace*{\fill} \par %插入空行 160 | \begin{center} 161 | \includegraphics{content/2/chapter6/images/5.jpg}\\ 162 | 图6.5 – 调整this指针的虚函数表 163 | \end{center} 164 | 165 | 函数调用现在变成如下方式:\par 166 | 167 | \begin{enumerate} 168 | \item 在虚函数表中查找函数指针。 169 | \item 调整this指针。 170 | \item 调用该方法。 171 | \end{enumerate} 172 | 173 | 这种方法还可以用于实现接口。因为接口只有方法,所以每个实现的接口都会向对象添加一个新的虚函数表指针。这更容易实现,而且可能更快,但它增加了每个对象实例的开销。在最坏的情况下,如果你的类有一个64位数据字段,实现了10个接口,那么你的对象需要96个字节的内存:类本身的vtable指针需要8个字节,数据成员需要8个字节,每个接口的vtable指针需要10 * 8个字节。\par 174 | 175 | 为了支持对对象的有意义的比较并执行运行时类型测试,需要首先对对象的指针进行标准化。如果我们在虚表中添加一个额外的字段,在对象的顶部包含一个偏移量,那么我们总是可以调整指针指向实际对象。在Circle类的虚函数表中,这个偏移量是0,但在嵌入式GraphicObj类的虚函数表中不是。当然,这是否需要实现取决于源语言的语义。\par 176 | 177 | LLVM本身并不支持面向对象特性的特殊实现。如本节所见,可以使用可用的LLVM数据类型实现所有方法。如果想尝试一种新的方法,那么一个好方法是先用C做一个原型。所需的指针操作可以快速地转换为LLVM IR,但是在高级语言中进行功能性的推理会更容易。\par 178 | 179 | 通过本节,您可以在自己的代码生成器中将编程语言中所有OOP构造下沉至LLVM IR中。您已经了解了如何表示单继承、使用接口的单继承或内存中的多继承,以及如何实现类型测试和如何查找虚函数的方法,这些都是OOP语言的核心概念。\par 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | -------------------------------------------------------------------------------- /content/2/chapter6/5.tex: -------------------------------------------------------------------------------- 1 | 本章中,您学习了如何将聚合数据类型和指针转换为LLVM IR代码。您还了解了ABI的复杂性。最后,您了解了将类和虚函数转换为LLVM IR的不同方法。有了本章的知识,你将能够为大多数真实的编程语言创建一个LLVM IR代码生成器。\par 2 | 3 | 下一章中,您将学习一些高级技术。异常处理在现代编程语言中相当常见,LLVM对它有一定的支持。将类型信息附加到指针可以进行某些优化,所以我们也将添加支持。最后,调试应用程序的能力对于许多开发人员来说是必不可少的,因此我们将在代码生成器中添加调试元数据的生成。\par 4 | 5 | \newpage -------------------------------------------------------------------------------- /content/2/chapter6/images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/2/chapter6/images/1.jpg -------------------------------------------------------------------------------- /content/2/chapter6/images/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/2/chapter6/images/2.jpg -------------------------------------------------------------------------------- /content/2/chapter6/images/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/2/chapter6/images/3.jpg -------------------------------------------------------------------------------- /content/2/chapter6/images/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/2/chapter6/images/4.jpg -------------------------------------------------------------------------------- /content/2/chapter6/images/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/2/chapter6/images/5.jpg -------------------------------------------------------------------------------- /content/2/chapter7/0.tex: -------------------------------------------------------------------------------- 1 | 通过前面章节介绍的中间表示(IR)生成,您已经可以实现编译器中所需的大部分功能。本章中,我们将讨论一些实际编译器中出现的主题,例如:许多现代语言都使用的异常处理,我们将研究如何将其转换为底层(LLVM)IR。\par 2 | 3 | 为了支持LLVM优化器在某些情况下生成更好的代码,我们向IR代码添加了额外的类型元数据,并附加调试元数据,使编译器的用户能够利用源代码级调试工具。\par 4 | 5 | 本章中,您将学习以下内容:\par 6 | 7 | \begin{itemize} 8 | \item 在抛出和捕获异常中,您将学习如何在编译器中实现异常处理 9 | \item 在为基于类型的别名分析生成元数据中,将添加额外的元数据到LLVM IR,这有助于LLVM更好地优化代码。 10 | \item 添加调试元数据,实现向生成的IR代码添加调试信息所需的类。 11 | \end{itemize} 12 | 13 | 本章的最后,将了解关于异常处理和基于类型别名分析,以及调试信息的元数据的知识。\par 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /content/2/chapter7/1.tex: -------------------------------------------------------------------------------- 1 | 本章的代码文件可在\url{https://github.com/PacktPublishing/Learn-LLVM-12/tree/master/Chapter07}获取。\par 2 | 3 | 你可以在视频中找到代码\url{https://bit.ly/3nllhED}。\par -------------------------------------------------------------------------------- /content/2/chapter7/3.tex: -------------------------------------------------------------------------------- 1 | 两个指针可以指向同一个内存单元,它们互为别名。在LLVM模型中没有输入内存,这使得优化器很难决定两个指针是否为别名。如果编译器能够证明两个指针没有别名,那么就有进行更多优化的可能。下一节中,在实现这种方法之前,我们将更仔细地研究这个问题,并研究添加的元数据将如何发挥作用。\par 2 | 3 | \hspace*{\fill} \par %插入空行 4 | \textbf{理解添加元数据的需求} 5 | 6 | 为了演示这个问题,让我们看看下面的函数:\par 7 | 8 | \begin{lstlisting}[caption={}] 9 | void doSomething(int *p, float *q) { 10 | *p = 42; 11 | *q = 3.1425; 12 | } 13 | \end{lstlisting} 14 | 15 | 优化器不能确定p和q指针是否指向相同的内存单元。优化过程中,这是一个重要的分析,称为别名分析。如果p和q指向同一个存储单元,那么它们就是别名。如果优化器能够证明两个指针从来没有别名,这将提供优化机会,例如:在soSomething()函数中,可以在不改变结果的情况下重新排序存储。\par 16 | 17 | 一种类型的变量是否可以是另一种类型的变量的别名,这取决于语言的定义。请注意,语言也可能包含打破基于类型的别名假设的表达式——例如,不相关类型之间的类型转换。\par 18 | 19 | LLVM开发人员选择的解决方案是添加元数据来加载和存储指令。元数据有两个用途:\par 20 | 21 | \begin{itemize} 22 | \item 首先,根据可以别名其他类型的类型层次结构定义类型层次结构 23 | \item 其次,描述了加载或存储指令中的内存访问 24 | \end{itemize} 25 | 26 | 让我们看看C中的类型层次结构。每种类型的层次结构都以一个根节点开始,根节点可以\textbf{命名},也可以\textbf{匿名}。LLVM假设具有相同名称的根节点描述相同类型的层次结构。您可以在同一个LLVM模块中使用不同的类型层次结构,并且LLVM做了一个安全的假设,即这些类型可以别名。在根节点之下,有用于标量类型的节点。聚合类型的节点不附加到根节点,但它们引用标量类型和其他聚合类型。Clang为C定义了如下层次结构:\par 27 | 28 | \begin{itemize} 29 | \item 根节点称为Simple C/C++ TBAA 30 | \item 根节点下面是用于字符类型的节点。这是C语言中的一种特殊类型,因为所有指针都可以转换为char类型的指针。 31 | \item 在char节点下面是用于其他标量类型的节点和用于所有指针的类型。 32 | \end{itemize} 33 | 34 | 聚合类型定义为成员类型和偏移量的序列。\par 35 | 36 | 这些元数据定义用于加载和存储指令的访问标记。访问标记由三部分组成:基类型、访问类型和偏移量。根据基类型的不同,访问标签描述内存访问有两种可能的方式:\par 37 | 38 | \begin{enumerate} 39 | \item 如果基类型是聚合类型,则访问标记描述结构成员的内存访问,具有访问类型并位于给定偏移量。 40 | \item 如果基类型是标量类型,则访问类型必须与基类型相同,并且偏移量必须为0。 41 | \end{enumerate} 42 | 43 | 有了这些定义,我们现在可以在访问标记上定义一个关系,该关系用于计算两个指针是否可以别名。元组(基类型,偏移量)的直接父类由基类型和偏移量决定:\par 44 | 45 | \begin{itemize} 46 | \item 如果基类型是标量类型并且偏移量为0,则直接父类型是(父类型,0),父类型是类型层次结构中定义的父节点的类型。如果偏移量不为0,则直接父节点未定义。 47 | \item 基类型是一个聚合类型,然后tuple(基类型,偏移量)的直接父类型是tuple(新类型,新偏移量),而新类型是偏移量处成员的类型。新偏移量会根据新开始调整的新类型的起始位置。 48 | \end{itemize} 49 | 50 | 这个关系的传递闭包是父关系。双内存访问类型——例如,(基类型1,访问类型1,偏移量1)和(基类型2,访问类型2,偏移量2)——可以别名为(基类型1,偏移量1)和(基类型2,偏移量2),反之亦然。这其实与父关系相关。\par 51 | 52 | 用一个例子来说明:\par 53 | 54 | \begin{lstlisting}[caption={}] 55 | struct Point { float x, y; } 56 | void func(struct Point *p, float *x, int *i, char *c) { 57 | p->x = 0; p->y = 0; *x = 0.0; *i = 0; *c = 0; 58 | } 59 | \end{lstlisting} 60 | 61 | 使用前面对标量类型的内存访问标记定义,参数i的访问标记是(int, int, 0),参数c的访问标记是(char, char, 0)。在类型层次结构中,int类型的节点的父节点是char节点,因此(int, 0)的直接父节点是(char, 0),两个指针都可以别名。参数x和参数c也是一样的,但是参数x和i不相关,因此它们不会互为别名。struct Point的y成员的访问是(Point、float、4),4是结构中y成员的偏移量。(Point, 4)的直接父对象是(float, 0),因此对p->y和x的访问可能会别名,而且出于同样的原因,参数c也是如此。\par 62 | 63 | 要创建元数据,我们使用llvm::MDBuilder类,在llvm/IR/MDBuilder.h头文件中声明。数据本身存储在llvm::MDNode和llvm::MDString类的实例中。使用构建器类可以让我们避开构造的内部细节。\par 64 | 65 | 通过调用createTBAARoot()方法创建根节点,该方法将类型层次结构的名称作为参数并返回根节点。可以使用createAnonymousTBAARoot()方法可以创建一个匿名的根节点。\par 66 | 67 | 使用createTBAAScalarTypeNode()方法将标量类型添加到层次结构中,该方法以类型的名称和父节点作为参数。为聚合类型添加类型节点稍微复杂一些,createTBAAStructTypeNode()方法以类型名称和字段列表作为参数。指定为std::pair instance。第一个元素表示成员的类型,第二个元素表示结构类型中的偏移量。\par 68 | 69 | 使用createTBAAStructTagNode()方法创建访问标记,该方法接受基类型、访问类型和偏移量作为参数。\par 70 | 71 | 最后,元数据必须附加到加载或存储指令。指令类有一个setMetadata()方法,用于添加各种元数据。第一个参数必须是llvm::LLVMContext::MD\underline{~}tbaa,第二个参数必须是访问标记。\par 72 | 73 | 有了这些知识,我们将在下一节中为tinylang添加基于类型的别名分析(TBAA)的元数据。\par 74 | 75 | \hspace*{\fill} \par %插入空行 76 | \textbf{向tinylang添加TBAA元数据} 77 | 78 | 为了支持TBAA,需要添加了一个新的CGTBAA类,该类负责生成元数据节点。我们让它成为CGModule类的成员,称它为TBAA。每个加载和存储指令都可能注释,为此我们在CGModule类中也放置了一个新函数。该函数试图创建标记访问信息。如果成功,则将元数据附加到指令。这种设计还允许我们在不需要元数据时关闭元数据生成——例如,在关闭了优化的构建中:\par 79 | 80 | \begin{lstlisting}[caption={}] 81 | void CGModule::decorateInst(llvm::Instruction *Inst, 82 | TypeDenoter *TyDe) { 83 | if (auto *N = TBAA.getAccessTagInfo(TyDe)) 84 | Inst->setMetadata(llvm::LLVMContext::MD_tbaa, N); 85 | } 86 | \end{lstlisting} 87 | 88 | 我们将新CGTBAA类的声明放入include/tinylang/CodeGen/CGTBAA.h头文件中,并将定义放入lib/CodeGen/CGTBAA.cpp文件中。除了抽象语法树(AST)定义之外,头文件还需要包含定义元数据节点和构建器的文件:\par 89 | 90 | \begin{lstlisting}[caption={}] 91 | #include "tinylang/AST/AST.h" 92 | #include "llvm/IR/MDBuilder.h" 93 | #include "llvm/IR/Metadata.h" 94 | \end{lstlisting} 95 | 96 | CGTBAA类需要存储一些数据成员,看看如何一步步地做到这些:\par 97 | 98 | \begin{enumerate} 99 | \item 首先,需要缓存类型层次结构的根: 100 | \begin{lstlisting}[caption={}] 101 | class CGTBAA { 102 | llvm::MDNode *Root; 103 | \end{lstlisting} 104 | 105 | \item 为了构造元数据节点,需要MDBuilder类的实例: 106 | \begin{lstlisting}[caption={}] 107 | llvm::MDBuilder MDHelper; 108 | \end{lstlisting} 109 | 110 | \item 最后,存储为类型生成的元数据以供重用: 111 | \begin{lstlisting}[caption={}] 112 | llvm::DenseMap 113 | MetadataCache; 114 | // … 115 | }; 116 | \end{lstlisting} 117 | \end{enumerate} 118 | 119 | 在定义了构造所需的变量之后,现在添加创建元数据所需的方法:\par 120 | 121 | 122 | \begin{enumerate} 123 | \item 构造函数初始化数据成员: 124 | \begin{lstlisting}[caption={}] 125 | CGTBAA::CGTBAA(llvm::LLVMContext &Ctx) 126 | : MDHelper(llvm::MDBuilder(Ctx)), Root(nullptr) {} 127 | \end{lstlisting} 128 | 129 | \item 惰性地实例化类型层次结构的根,将其命名为Simple tinylang TBAA: 130 | \begin{lstlisting}[caption={}] 131 | llvm::MDNode *CGTBAA::getRoot() { 132 | if (!Root) 133 | Root = MDHelper.createTBAARoot("Simple tinylang 134 | TBAA"); 135 | return Root; 136 | } 137 | \end{lstlisting} 138 | 139 | \item 对于标量类型,在MDBuilder类的帮助下根据类型的名称创建元数据节点。新的元数据节点存储在缓存中: 140 | \begin{lstlisting}[caption={}] 141 | llvm::MDNode * 142 | CGTBAA::createScalarTypeNode(TypeDeclaration *Ty, 143 | StringRef Name, 144 | llvm::MDNode *Parent) { 145 | llvm::MDNode *N = 146 | MDHelper.createTBAAScalarTypeNode(Name, Parent); 147 | return MetadataCache[Ty] = N; 148 | } 149 | \end{lstlisting} 150 | 151 | \item 为记录创建元数据的方法更为复杂,必须枚举记录的所有字段: 152 | \begin{lstlisting}[caption={}] 153 | llvm::MDNode *CGTBAA::createStructTypeNode( 154 | TypeDeclaration *Ty, StringRef Name, 155 | llvm::ArrayRef> 157 | Fields) { 158 | llvm::MDNode *N = 159 | MDHelper.createTBAAStructTypeNode(Name, Fields); 160 | return MetadataCache[Ty] = N; 161 | } 162 | \end{lstlisting} 163 | 164 | \item 要返回tinylang类型的元数据,需要创建类型层次结构。由于tinylang的类型系统非常有限,可以使用一种简单的方法。每个标量类型映射到附加到根节点的唯一类型,并将所有指针映射到单个类型。结构化类型然后引用这些节点。如果不能映射类型,则返回nullptr: 165 | \begin{lstlisting}[caption={}] 166 | llvm::MDNode *CGTBAA::getTypeInfo(TypeDeclaration *Ty) { 167 | if (llvm::MDNode *N = MetadataCache[Ty]) 168 | return N; 169 | 170 | if (auto *Pervasive = 171 | llvm::dyn_cast(Ty)) { 172 | StringRef Name = Pervasive->getName(); 173 | return createScalarTypeNode(Pervasive, Name, 174 | getRoot()); 175 | } 176 | if (auto *Pointer = 177 | llvm::dyn_cast(Ty)) { 178 | StringRef Name = "any pointer"; 179 | return createScalarTypeNode(Pointer, Name, 180 | getRoot()); 181 | } 182 | if (auto *Record = 183 | llvm::dyn_cast(Ty)) { 184 | llvm::SmallVector, 186 | 4> 187 | Fields; 188 | auto *Rec = 189 | llvm::cast( 190 | CGM.convertType(Record)); 191 | const llvm::StructLayout *Layout = 192 | CGM.getModule()->getDataLayout() 193 | .getStructLayout(Rec); 194 | 195 | unsigned Idx = 0; 196 | for (const auto &F : Record->getFields()) { 197 | uint64_t Offset = Layout->getElementOffset(Idx); 198 | Fields.emplace_back(getTypeInfo(F.getType()), 199 | Offset); 200 | ++Idx; 201 | } 202 | StringRef Name = CGM.mangleName(Record); 203 | return createStructTypeNode(Record, Name, Fields); 204 | } 205 | return nullptr; 206 | } 207 | \end{lstlisting} 208 | 209 | \item 获取元数据的一般方法是getAccessTagInfo()。因为只需要查找指针类型,所以我们检查它。否则,返回nullptr: 210 | \begin{lstlisting}[caption={}] 211 | llvm::MDNode *CGTBAA::getAccessTagInfo(TypeDenoter *TyDe) 212 | { 213 | if (auto *Pointer = llvm::dyn_cast(TyDe)) 214 | { 215 | return getTypeInfo(Pointer->getTyDen()); 216 | } 217 | return nullptr; 218 | } 219 | \end{lstlisting} 220 | \end{enumerate} 221 | 222 | 要启用TBAA元数据的生成,只需要将元数据附加到生成的加载和存储指令。例如,在CGProced\allowbreak ure::writeVariable()中,对全局变量进行存储,使用存储指令:\par 223 | 224 | \begin{lstlisting}[caption={}] 225 | Builder.CreateStore(Val, CGM.getGlobal(D)); 226 | \end{lstlisting} 227 | 228 | 为了修饰指令,需要将前面的行替换为以下行:\par 229 | 230 | \begin{lstlisting}[caption={}] 231 | auto *Inst = Builder.CreateStore(Val, 232 | CGM.getGlobal(Decl)); 233 | CGM.decorateInst(Inst, V->getTypeDenoter()); 234 | \end{lstlisting} 235 | 236 | 有了这些更改,就完成了TBAA元数据的生成。\par 237 | 238 | 下一节中,我们将讨论一个非常类似的主题:对元数据的生成进行调试。\par 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /content/2/chapter7/5.tex: -------------------------------------------------------------------------------- 1 | 本章中,您学习了如何在LLVM中抛出和捕获异常,以及需要生成哪些IR代码来利用该特性。为了增强IR的范围,了解了如何将各种元数据附加到指令中。基于类型的别名的元数据为LLVM优化器提供了额外的信息,并有助于进行某些优化,以生成更好的机器码。用户总是喜欢使用源代码级调试器,通过向IR代码添加调试信息,就能够支持这一重要特性。\par 2 | 3 | 优化IR代码是LLVM的核心任务。下一章中,我们将学习Pass管理器是如何工作的,以及如何影响Pass管理器所管理的优化管道。\par 4 | 5 | \newpage -------------------------------------------------------------------------------- /content/2/chapter8/0.tex: -------------------------------------------------------------------------------- 1 | LLVM使用一系列Pass来优化\textbf{中间表示(IR)},通对IR的单元(函数或模块)执行操作。操作可以是转换(以定义的方式更改IR),也可以是分析(收集依赖关系等信息)。一系列的Pass称为\textbf{Pass流水线}。Pass管理器在编译器生成的IR上执行Pass管道。因此,了解Pass管理器做什么,以及如何构建Pass管道很重要。支持编程语言的语义可能需要开发新的Pass,所以必须将这些Pass添加到管道中。\par 2 | 3 | 本章中,我们将包括以下内容:\par 4 | 5 | \begin{itemize} 6 | \item 介绍LLVM Pass管理器 7 | \item 使用新Pass管理器实现一个Pass 8 | \item 使用旧Pass管理器中使用Pass 9 | \item 向编译器添加优化流水线 10 | \end{itemize} 11 | 12 | 本章结束时,将了解如何开发一个新的Pass,以及如何将它添加到Pass流水线中。还将了解如何在自己的编译器中设置Pass管道。\par 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /content/2/chapter8/1.tex: -------------------------------------------------------------------------------- 1 | 本章的代码文件可在\url{https://github.com/PacktPublishing/Learn-LLVM-12/tree/master/Chapter08}获取。\par 2 | 3 | 你可以在视频中找到代码\url{https://bit.ly/3nllhED}。\par -------------------------------------------------------------------------------- /content/2/chapter8/2.tex: -------------------------------------------------------------------------------- 1 | LLVM核心库优化编译器创建的IR,并将其转换为目标代码。这个巨大的任务可以分解成几个单独的步骤,称为Pass。这些Pass需要按照正确的顺序执行,这是Pass管理器的目标。\par 2 | 3 | 但为什么不硬编码Pass的顺序呢?编译器的用户通常期望编译器提供不同级别的优化。开发人员在开发期间进行优化,喜欢更快的编译速度。最终的应用程序应该尽可能快地运行,编译器应该能够执行复杂的优化,并接受更长的编译时间。不同的优化级别意味着需要执行的优化通过的数量不同。而且,作为编译器作者,您可能希望提供自己的Pass,可以充分展现您对源语言的了解。例如,您可能想用内联IR或(如果可能的话)用该函数的计算结果替换已知的库函数。对于C语言,这样的Pass是LLVM核心库的一部分,但是对于其他语言,您需要单独提供。在介绍自己的Pass时,您可能需要重新排序或添加一些Pass。例如,如果知道Pass的操作使某些IR代码不可访问,那么您应该运行完自己的Pass之后删除Pass。这里,Pass管理器可以帮助您进行管理。\par 4 | 5 | Pass通常根据作用范围进行分类:\par 6 | 7 | \begin{itemize} 8 | \item Pass函数接受单个函数作为输入,并仅对该函数执行。 9 | \item Pass模块接受整个模块作为输入。这样的Pass在给定的模块上执行,并可用于该模块中的过程内操作。 10 | \item 调用Pass图以自底向上的顺序遍历调用图的函数。 11 | \end{itemize} 12 | 13 | 除了IR代码之外,Pass还可能消耗、生成或使一些分析结果无效。有很多不同的分析,例如:别名分析或支配树的构造。支配树帮助将不变代码移出循环,因此执行这种转换的Pass只能在创建支配树之后运行。另一个Pass可能执行可能使现有支配树无效的转换。\par 14 | 15 | 在编译器内部,Pass管理器有以下功能:\par 16 | 17 | \begin{itemize} 18 | \item 分析结果由各Pass共享。这需要您指定跟踪哪个Pass需要哪个分析,以及每个分析的状态。其目标是避免不必要的分析重新计算,并尽快释放分析结果所占用的内存。 19 | 20 | \item Pass以流水线方式执行,例如:如果需要依次执行几个Pass函数,那么Pass管理器将在第一个函数上运行这些函数中的每个Pass。然后,将在第二个函数上运行所有的Pass函数,以此类推。这里的基本思想是改进缓存行为,因为编译器只对有限的数据集(即一个IR函数)执行转换,然后转向下一个(有限的)数据集。 21 | \end{itemize} 22 | 23 | LLVM中有两个Pass管理器: \par 24 | 25 | \begin{itemize} 26 | \item 旧(或遗留的)Pass管理器 27 | \item 新Pass管理器 28 | \end{itemize} 29 | 30 | 未来是属于新的Pass管理器的,但目前过渡尚未完成。许多关键的Pass(例如目标代码生成)还没有迁移到新Pass管理器,因此理解这两个Pass管理器还挺重要的。\par 31 | 32 | 旧Pass管理器要求Pass从基类继承,例如:Pass函数从llvm::FunctionPass类继承。相比之下,新Pass管理器基于概念的方法,只需要从特殊的llvm::PassInfo<>语言特性(mixin)类继承。旧Pass管理器没有明确表示Pass之间的依赖关系,而新Pass管理器中,依赖关系需要显式编码。新Pass管理器还具有处理分析的不同方法,并允许通过命令行上的文本表示规范优化流水。一些LLVM用户报告说,仅仅从旧Pass管理器切换到新Pass管理器,编译量就减少了10\%,这是使用新Pass管理器非常有力的理由。\par 33 | 34 | 首先,我们将为新的Pass管理器实现一个Pass,并探索如何将它添加到优化流水中。稍后,我们还将看看如何使用旧Pass管理器。\par 35 | 36 | -------------------------------------------------------------------------------- /content/2/chapter8/3.tex: -------------------------------------------------------------------------------- 1 | 2 | Pass可以在LLVM IR上执行任意的转换。为了说明添加新Pass的机制,我们的新Pass只计算IR指令和基本块的数量,我们将这个Pass命名为countir。将Pass添加到LLVM源树或作为一个独立的Pass略有不同,因此我们将在以下部分中进行这两种操作。让我们从向LLVM源树添加一个新的Pass开始。\par 3 | 4 | \hspace*{\fill} \par %插入空行 5 | \textbf{向LLVM源树添加一个Pass} 6 | 7 | 我们从将新的Pass添加到LLVM源开始。如果稍后想要在LLVM树中发布新的Pass,这是一种正确的方法。\par 8 | 9 | 在LLVM IR上执行转换的Pass的源代码位于llvm-project/llvm/lib/Transforms文件夹,而头文件位于llvm-project/llvm/include/llvm/Transforms文件夹。因为有这么多的Pass,他们已分类到对应类别的子文件夹。\par 10 | 11 | 对于我们的新Pass,需要在两个位置都创建了一个名为CountIR的新文件夹。首先,实现CountIR.h头文件:\par 12 | 13 | \begin{enumerate} 14 | \item 像往常一样,需要确保文件可以多次包含。另外,我们需要包含Pass管理器定义: 15 | \begin{lstlisting}[caption={}] 16 | #ifndef LLVM_TRANSFORMS_COUNTIR_COUNTIR_H 17 | #define LLVM_TRANSFORMS_COUNTIR_COUNTIR_H 18 | 19 | #include "llvm/IR/PassManager.h" 20 | \end{lstlisting} 21 | 22 | \item 因为在LLVM源代码中,所以可以将新的CountIR类放入LLVM命名空间中。该类继承自PassInfoMixin模板。这个模板只添加了一些样板代码,比如name()方法。不过,它不用于确定Pass的类型: 23 | \begin{lstlisting}[caption={}] 24 | namespace llvm { 25 | class CountIRPass : public PassInfoMixin { 26 | \end{lstlisting} 27 | 28 | \item 在运行时,任务将调用的run()方法。run()方法的签名决定了Pass的类型。这里,第一个参数是函数类型的引用,所以这是一个Pass函数: 29 | \begin{lstlisting}[caption={}] 30 | public: 31 | PreservedAnalyses run(Function &F, 32 | FunctionAnalysisManager &AM); 33 | \end{lstlisting} 34 | 35 | \item 最后,我们需要闭合类、命名空间和头文件的宏: 36 | \begin{lstlisting}[caption={}] 37 | }; 38 | } // namespace llvm 39 | #endif 40 | \end{lstlisting} 41 | 当然,新Pass的定义非常简单,只执行了一项简单的任务。\par 42 | 43 | 继续在CountIIR.cpp文件中实现Pass。如果在调试模式下编译,LLVM会收集关于Pass的统计信息。对于我们的Pass,会使用这个基础组件。\par 44 | 45 | \item 通过包含我们自己的头文件和所需的LLVM头文件来开始编写源代码: 46 | \begin{lstlisting}[caption={}] 47 | #include "llvm/Transforms/CountIR/CountIR.h" 48 | #include "llvm/ADT/Statistic.h" 49 | #include "llvm/Support/Debug.h" 50 | \end{lstlisting} 51 | 52 | \item 为了缩短源代码,我们告诉编译器我们使用的是llvm名称空间: 53 | \begin{lstlisting}[caption={}] 54 | using namespace llvm; 55 | \end{lstlisting} 56 | 57 | \item LLVM的内置调试基础设施要求我们定义一个调试类型,它是一个字符串。这个字符串稍后会显示在打印的统计信息中: 58 | \begin{lstlisting}[caption={}] 59 | #define DEBUG_TYPE "countir" 60 | \end{lstlisting} 61 | 62 | \item 我们用STATISTIC宏定义两个计数器变量。第一个参数是计数器变量的名称,第二个参数是将在统计中打印的文本: 63 | \begin{lstlisting}[caption={}] 64 | STATISTIC(NumOfInst, "Number of instructions."); 65 | STATISTIC(NumOfBB, "Number of basic blocks."); 66 | \end{lstlisting} 67 | 68 | \item 在run()方法中,我们循环遍历函数的所有基本块,并递增相应的计数器,并对基本块的所有指令都做同样的操作。为了防止编译器对未使用的变量发出警告,我们插入了I变量做空操作。因为我们只计算IR而不更改IR,所以我们告诉调用者使用了Pass,并且保留了所有的分析: 69 | \begin{lstlisting}[caption={}] 70 | PreservedAnalyses 71 | CountIRPass::run(Function &F, 72 | FunctionAnalysisManager &AM) { 73 | for (BasicBlock &BB : F) { 74 | ++NumOfBB; 75 | for (Instruction &I : BB) { 76 | (void)I; 77 | ++NumOfInst; 78 | } 79 | } 80 | return PreservedAnalyses::all(); 81 | } 82 | \end{lstlisting} 83 | 84 | \end{enumerate} 85 | 86 | 目前为止,已经实现了新Pass的功能。稍后将对out-of-tree Pass重用此实现。对于LLVM树中的解决方案,必须更改LLVM中的几个文件来声明新的Pass:\par 87 | 88 | \begin{enumerate} 89 | \item 首先,需要将CMakeLists.txt添加到源文件夹。这个文件包含一个新的LLVM库名称LLVM\allowbreak CountIR的构建说明。新库需要链接到LLVM Support组件,因为我们使用了调试和统计基础设施,还需要链接到LLVM Core组件,其中包含LLVM IR的定义: 90 | \begin{tcolorbox}[colback=white,colframe=black] 91 | add\underline{~}llvm\underline{~}component\underline{~}library(LLVMCountIR \\ 92 | \hspace*{0.5cm}CountIR.cpp \\ 93 | \hspace*{0.5cm}LINK\underline{~}COMPONENTS Core Support ) 94 | \end{tcolorbox} 95 | 96 | \item 为了使这个新的库成为构建的一部分,我们需要将这个文件夹添加到父文件夹的CMakeList.txt中,即llvm-project/llvm/lib/Transforms/CMakeList.txt文件。然后,添加以下行: 97 | \begin{tcolorbox}[colback=white,colframe=black] 98 | add\underline{~}subdirectory(CountIR) 99 | \end{tcolorbox} 100 | 101 | \item PassBuilder类需要知道我们的新Pass。为此,在llvm-project/llvm/lib/Passes/PassBuilder.cpp文件的include部分添加以下代码: 102 | \begin{lstlisting}[caption={}] 103 | #include "llvm/Transforms/CountIR/CountIR.h 104 | \end{lstlisting} 105 | 106 | \item 最后一步,需要更新Pass注册表,它位于ellvmproject/llvm/lib/Passes/PassRegistry.def文件中。查找定义Pass函数的部分,例如:通过搜索function \underline{~}PASS宏。本节中,需要添加以下行: 107 | \begin{lstlisting}[caption={}] 108 | FUNCTION_PASS("countir", CountIRPass()) 109 | \end{lstlisting} 110 | 111 | \item 我们现在已经做了所有必要的改变。按照第1章的构建说明,使用CMake重新编译LLVM。为了测试新的Pass,我们在演示中存储以下IR代码。Ll文件在构建文件夹中。代码有两个函数,三个指令和两个基本块: 112 | \begin{tcolorbox}[colback=white,colframe=black] 113 | define internal i32 @func() \{ \\ 114 | \hspace*{0.5cm}ret i32 0 \\ 115 | \} \\ 116 | \\ 117 | define dso\underline{~}local i32 @main() \{ \\ 118 | \hspace*{0.5cm} \%1 = call i32 @func() \\ 119 | \hspace*{0.5cm} ret i32 \%1 \\ 120 | \} 121 | \end{tcolorbox} 122 | 123 | \item 我们可以通过opt实用程序使用新Pass。要运行新Pass,我们要使用\verb|--|passes="countir"选项。要获得统计输出,需要添加\verb|--|stats选项。因为我们不需要生成的比特码,所以我们也指定了 \verb|--|disable-output选项: 124 | \begin{tcolorbox}[colback=white,colframe=black] 125 | \$ bin/opt \verb|--|disable-output \verb|--|passes="countir" \verb|--|stats \\ 126 | demo.ll \\ 127 | \verb|===------------------------------------------------------| \\ 128 | \verb|--===| \\ 129 | ... Statistics Collected ... \\ 130 | \verb|===------------------------------------------------------| \\ 131 | \verb|--===| \\ 132 | 2 countir - Number of basic blocks. \\ 133 | 3 countir - Number of instructions. 134 | \end{tcolorbox} 135 | 136 | \item 运行我们的新Pass,其输出符合期望。我们已经成功扩展了LLVM! 137 | 138 | \end{enumerate} 139 | 140 | 运行单个Pass有助于调试。使用\verb|--|passes选项,不仅可以命名单个Pass,还可以描述整个流水。例如,优化级别2的默认管道名为default。可以使用\verb|--|passes="module(countir),default"参数在默认管道之前运行countir Pass,这样的流水描述中的Pass名称必须是相同类型的。默认的流水是一个模块Pass,我们的countir Pass是一个Pass函数。要从两者创建模块管道。首先,必须创建一个包含countir Pass的Pass模块。通过模块(countir)可以完成,通过在逗号分隔的列表中指定函数,可以向模块Pass中添加更多的Pass函数。以同样的方式,可以组合Pass模块。想要研究效果的话,可以使用内联和countir Pass,以不同的顺序运行它们,或者作为Pass模块,结果会是不同的统计输出。\par 141 | 142 | 如果您计划将Pass作为LLVM的一部分发布,那么向LLVM源树中添加一个新的Pass是有意义的。如果不打算这样做,或者想独立于LLVM分发Pass,那么可以创建一个Pass插件。下一节中,我们将查看执行此操作的步骤。\par 143 | 144 | 145 | \hspace*{\fill} \par %插入空行 146 | \textbf{添加一个新Pass作为插件} 147 | 148 | 为了提供一个新的Pass作为插件,我们将创建一个使用LLVM的新项目:\par 149 | 150 | \begin{enumerate} 151 | \item 我们首先在源文件夹中创建一个名为countirpass的文件夹。该文件夹将具有以下结构和文件: 152 | \begin{tcolorbox}[colback=white,colframe=black] 153 | |\verb|--| CMakeLists.txt \\ 154 | |\verb|--| include \\ 155 | |\hspace{1cm}|\verb|--| CountIR.h \\ 156 | |\verb|--| lib \\ 157 | \hspace*{0.8cm}|\verb|--| CMakeLists.txt \\ 158 | \hspace*{0.8cm}|\verb|--| CountIR.cpp 159 | \end{tcolorbox} 160 | 161 | \item 注意,我们重用了前一节中的功能,并进行了一些调整。CountIR.h头文件现在位于不同的位置,因此我们更改了用作保护宏的名称。我们也不使用llvm名称空间,因为现在在llvm源之外。头文件如下所示: 162 | \begin{lstlisting}[caption={}] 163 | #ifndef COUNTIR_H 164 | #define COUNTIR_H 165 | 166 | #include "llvm/IR/PassManager.h" 167 | 168 | class CountIRPass 169 | : public llvm::PassInfoMixin { 170 | public: 171 | llvm::PreservedAnalyses 172 | run(llvm::Function &F, 173 | llvm::FunctionAnalysisManager &AM); 174 | }; 175 | 176 | #endif 177 | \end{lstlisting} 178 | 179 | \item 可以复制上一节中的CountIR.cpp实现文件。这里也需要一些小的变动。因为头文件已经改变了,需要用下面的代码替换include指令: 180 | \begin{lstlisting}[caption={}] 181 | #include "CountIR.h" 182 | \end{lstlisting} 183 | 184 | \item 我们还需要在Pass构建器中注册新Pass,当加载插件时就会发生这种情况。Pass插件管理器调用特殊函数llvmGetPassPluginInfo(),该函数会执行注册。对于这个实现,需要额外的包含两个文件: 185 | \begin{lstlisting}[caption={}] 186 | #include "llvm/Passes/PassBuilder.h" 187 | #include "llvm/Passes/PassPlugin.h" 188 | \end{lstlisting} 189 | 用户使用\verb|–-|passes选项指定要在命令行上运行的Pass。PassBuilder类从字符串中提取Pass名称。为了创建一个名为Pass的实例,PassBuilder类维护一个回调列表。其实,调用回调时使用的是Pass名称和Pass管理器。如果回调知道Pass名称,那么将此Pass的一个实例添加到Pass管理器中。对于Pass,需要提供这样一个回调函数: 190 | \begin{lstlisting}[caption={}] 191 | bool PipelineParsingCB( 192 | StringRef Name, FunctionPassManager &FPM, 193 | ArrayRef) { 194 | if (Name == "countir") { 195 | FPM.addPass(CountIRPass()); 196 | return true; 197 | } 198 | return false; 199 | } 200 | \end{lstlisting} 201 | 202 | \item 当然,需要将这个函数注册为PassBuilder实例。加载插件后,注册回调函数就是为了这个目的。我们的注册功能如下: 203 | \begin{lstlisting}[caption={}] 204 | void RegisterCB(PassBuilder &PB) { 205 | PB.registerPipelineParsingCallback(PipelineParsingCB); 206 | } 207 | \end{lstlisting} 208 | 209 | \item 最后,每个插件都需要提供前面提到的llvmGetPassPluginInfo()函数。这个函数返回一个包含四个元素的结构:我们的插件使用的LLVM插件API版本、一个名称、插件的版本号和注册回调。插件API要求函数使用extern“C”。这是为了避免C++修饰命名的问题。功能非常简单: 210 | \begin{lstlisting}[caption={}] 211 | extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_ 212 | WEAK 213 | llvmGetPassPluginInfo() { 214 | return {LLVM_PLUGIN_API_VERSION, "CountIR", "v0.1", 215 | RegisterCB}; 216 | } 217 | \end{lstlisting} 218 | 为每个回调函数实现一个单独的函数有助于理解发生了什么。如果插件提供了几个Pass,那么可以扩展RegisterCB回调函数来注册所有Pass。通常,可以找到一种紧凑的方法。下面的llvmGetPassPluginInfo()函数将前面的PipelineParsingCB()、RegisterCB()和llvmGetPass\allowbreak PluginInfo()组合成一个函数,并通过Lambda函数来实现: 219 | \begin{lstlisting}[caption={}] 220 | extern "C" ::llvm::PassPluginLibraryInfo LLVM_ATTRIBUTE_ 221 | WEAK 222 | llvmGetPassPluginInfo() { 223 | return {LLVM_PLUGIN_API_VERSION, "CountIR", "v0.1", 224 | [](PassBuilder &PB) { 225 | PB.registerPipelineParsingCallback( 226 | [](StringRef Name, FunctionPassManager 227 | &FPM, 228 | ArrayRef) 229 | { 230 | if (Name == "countir") { 231 | FPM.addPass(CountIRPass()); 232 | return true; 233 | } 234 | return false; 235 | }); 236 | }}; 237 | } 238 | \end{lstlisting} 239 | 240 | \item 现在,只需要添加构建文件。这个lib/CMakeLists.txt文件只包含一个编译源文件的命令。特定于llvm的命令add\underline{~}llvm\underline{~}library()确保使用了与构建llvm时相同的编译器标志: 241 | \begin{tcolorbox}[colback=white,colframe=black] 242 | add\underline{~}llvm\underline{~}library(CountIR MODULE CountIR.cpp) 243 | \end{tcolorbox} 244 | 顶层CMakeLists.txt文件更复杂。 245 | 246 | \item 通常,我们设置了所需的CMake版本和项目名称。另外,将LLVM\underline{~}EXPORTED\underline{~}SYMBOL\underline{~}FILE变量设置为ON。这是插件在Windows上工作的必要条件: 247 | \begin{tcolorbox}[colback=white,colframe=black] 248 | cmake\underline{~}minimum\underline{~}required(VERSION 3.4.3) \\ 249 | project(countirpass)\\ 250 | \\ 251 | set(LLVM\underline{~}EXPORTED\underline{~}SYMBOL\underline{~}FILE ON) 252 | \end{tcolorbox} 253 | 254 | \item 接下来,安装LLVM。我们还要找到的版本信息,并打印到控制台: 255 | \begin{tcolorbox}[colback=white,colframe=black] 256 | find\underline{~}package(LLVM REQUIRED CONFIG) \\ 257 | message(STATUS "Found LLVM \$\{LLVM\underline{~}PACKAGE\underline{~}VERSION\}") \\ 258 | message(STATUS "Using LLVMConfig.cmake in: \$\{LLVM\underline{~}DIR\}") 259 | \end{tcolorbox} 260 | 261 | \item 现在,可以将LLVM中的cmake文件夹添加到搜索路径中。包括了特定于llvm的文件ChooseMSVCCRT和AddLLVM,并提供了额外的命令: 262 | \begin{tcolorbox}[colback=white,colframe=black] 263 | list(APPEND CMAKE\underline{~}MODULE\underline{~}PATH \$\{LLVM\underline{~}DIR\}) \\ 264 | include(ChooseMSVCCRT) \\ 265 | include(AddLLVM) 266 | \end{tcolorbox} 267 | 268 | \item 编译器需要知道所需的定义和LLVM的路径: 269 | \begin{tcolorbox}[colback=white,colframe=black] 270 | include\underline{~}directories("\$\{LLVM\underline{~}INCLUDE\underline{~}DIR\}") \\ 271 | add\underline{~}definitions("\$\{LLVM\underline{~}DEFINITIONS\}") \\ 272 | link\underline{~}directories("\$\{LLVM\underline{~}LIBRARY\underline{~}DIR\}") 273 | \end{tcolorbox} 274 | 275 | \item 最后,我们添加自己的include和source文件夹: 276 | \begin{tcolorbox}[colback=white,colframe=black] 277 | include\underline{~}directories(BEFORE include) \\ 278 | add\underline{~}subdirectory(lib) 279 | \end{tcolorbox} 280 | 281 | \item 实现了所有必需的文件之后,现在可以在countirpass文件夹旁创建build文件夹了。首先,切换到构建目录并创建构建文件: 282 | \begin{tcolorbox}[colback=white,colframe=black] 283 | \$ cmake –G Ninja ../countirpass 284 | \end{tcolorbox} 285 | 286 | \item 然后,可以编译插件: 287 | \begin{tcolorbox}[colback=white,colframe=black] 288 | \$ ninja 289 | \end{tcolorbox} 290 | 291 | \item 使用插件与opt工具,这是模块化的LLVM优化器和分析器。其中,opt生成输入文件的优化版本。使用插件时,需要指定一个参数来加载插件: 292 | \begin{tcolorbox}[colback=white,colframe=black] 293 | \$ opt \verb|--|load-pass-plugin=lib/CountIR.so \\ 294 | \verb|--|passes="countir"$\setminus$ \\ 295 | \hspace*{0.5cm}\verb|--|disable-output \verb|–-|stats demo.ll 296 | \end{tcolorbox} 297 | 298 | \end{enumerate} 299 | 300 | 输出与以前的版本相同。恭喜你,Pass插件工作了!\par 301 | 302 | 到目前为止,我们只为新Pass管理器创建了一个Pass。在下一节中,我们还将扩展旧Pass管理器中的Pass。\par 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | -------------------------------------------------------------------------------- /content/2/chapter8/4.tex: -------------------------------------------------------------------------------- 1 | 2 | 未来是属于新的Pass管理器,专为旧Pass管理器开发一个新的Pass没有什么意义。然而,在正在进行的转换阶段,如果Pass可以与两个Pass管理器一起工作,就很有用,因为LLVM中的大多数Pass已经这样做了。\par 3 | 4 | 旧Pass管理器需要从某些基类派生的Pass,例如:Pass函数必须派生自FunctionPass基类。还有更多的差异,Pass管理器运行的方法名为runOnFunction(),还必须提供Pass的ID。在这里遵循的策略是创建一个单独的类,可以在旧Pass管理器中使用,并以一种可以在两个Pass管理器中使用的功能的方式重构源码。\par 5 | 6 | 我们使用Pass插件作为基础。在include/CountIR.h头文件中,我们添加了一个新的类定义,如下所示:\par 7 | 8 | \begin{enumerate} 9 | \item 这个新类需要从FunctionPass类派生,所以包含了一个额外的头文件来获取类定义: 10 | \begin{lstlisting}[caption={}] 11 | #include "llvm/Pass.h" 12 | \end{lstlisting} 13 | 14 | \item 我们将这个新类命名为CountIRLegacyPass。类需要一个ID作为内部LLVM机制,用它初始化父类: 15 | \begin{lstlisting}[caption={}] 16 | class CountIRLegacyPass : public llvm::FunctionPass { 17 | public: 18 | static char ID; 19 | CountIRLegacyPass() : llvm::FunctionPass(ID) {} 20 | \end{lstlisting} 21 | 22 | \item 为了实现Pass功能,必须重写两个函数。每个LLVM IR函数都会调用runOnFunction()方法,并实现计数功能。getAnalysisUsage()方法用来声明所有的分析结果都保存了: 23 | \begin{lstlisting}[caption={}] 24 | bool runOnFunction(llvm::Function &F) override; 25 | void getAnalysisUsage(llvm::AnalysisUsage &AU) const 26 | override; 27 | }; 28 | \end{lstlisting} 29 | 30 | \item 对头文件的更改完成后,可以增强lib/CountIR.cpp文件中的实现。为了重用计数功能,我们将源代码移动到一个新函数中: 31 | \begin{lstlisting}[caption={}] 32 | void runCounting(Function &F) { 33 | for (BasicBlock &BB : F) { 34 | ++NumOfBB; 35 | for (Instruction &I : BB) { 36 | (void)I; 37 | ++NumOfInst; 38 | } 39 | } 40 | } 41 | \end{lstlisting} 42 | 43 | \item 为了使用新函数,新Pass管理器的方法需要更新: 44 | \begin{lstlisting}[caption={}] 45 | PreservedAnalyses 46 | CountIRPass::run(Function &F, FunctionAnalysisManager 47 | &AM) { 48 | runCounting(F); 49 | return PreservedAnalyses::all(); 50 | } 51 | \end{lstlisting} 52 | 53 | \item 以同样的方式,为旧Pass管理器实现方法。使用false返回值,表示IR没有改变: 54 | \begin{lstlisting}[caption={}] 55 | bool CountIRLegacyPass::runOnFunction(Function &F) { 56 | runCounting(F); 57 | return false; 58 | } 59 | \end{lstlisting} 60 | 61 | \item 为了保留现有的分析结果,必须以以下方式实现getAnalysisUsage()方法。这类似于新Pass管理器中的PreservedAnalyses::all()返回值。如果不实现这个方法,那么默认情况下所有的分析结果都会丢弃: 62 | \begin{lstlisting}[caption={}] 63 | void CountIRLegacyPass::getAnalysisUsage( 64 | AnalysisUsage &AU) const { 65 | AU.setPreservesAll(); 66 | } 67 | \end{lstlisting} 68 | 69 | \item ID字段可以用任意值初始化,因为LLVM使用该字段的地址。公共值是0,所以可以直接使用: 70 | \begin{lstlisting}[caption={}] 71 | char CountIRLegacyPass::ID = 0; 72 | \end{lstlisting} 73 | 74 | \item 现在只有Pass注册没有了。要注册新Pass,需要提供RegisterPass<>模板的静态实例。第一个参数是调用新Pass的命令行选项的名称。第二个参数是Pass的名称,是在使用-help选项时为用户提供信息使用。 75 | \begin{lstlisting}[caption={}] 76 | static RegisterPass 77 | X("countir", "CountIR Pass"); 78 | \end{lstlisting} 79 | 80 | \item 这些更改足以让我们在旧Pass管理器和新Pass管理器下使用新的Pass。要测试添加的内容,请回到构建文件夹并编译Pass: 81 | \begin{tcolorbox}[colback=white,colframe=black] 82 | \$ ninja 83 | \end{tcolorbox} 84 | 85 | \item 为了在旧Pass管理器中加载插件,需要使用\verb|--|load选项。新Pass是可以通过\verb|--|countir选项调用: 86 | \begin{tcolorbox}[colback=white,colframe=black] 87 | \$ opt \verb|--|load-pass-plugin=lib/CountIR.so \verb|--|countir \verb|–-|stats$\setminus$ \\ 88 | \hspace*{0.5cm}\verb|--|disable-output demo.ll 89 | \end{tcolorbox} 90 | 91 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black, title=Tip] 92 | 还请在前一节的命令行中检查,使用新Pass管理器调用我们的Pass仍然可以正常工作! 93 | \end{tcolorbox} 94 | 95 | \end{enumerate} 96 | 97 | 能够使用llvm提供的工具运行新Pass非常香,但最终,我们希望在我们的编译器中运行它。在下一节中,我们将探讨如何设置优化和如何自定义流水。\par 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /content/2/chapter8/6.tex: -------------------------------------------------------------------------------- 1 | 本章中,您学习了如何为LLVM创建一个新的Pass。您使用一个Pass流水描述和一个扩展点运行Pass。通过构造和执行类似于Clang的Pass流水,扩展了编译器,将tinylang变成了带优化的编译器。Pass流水允许在扩展点添加Pass,并且您了解了如何在这些点注册Pass。这使您能够使用自己开发的Pass,或对现有Pass进行扩展或优化流水。\par 2 | 3 | 下一章中,我们将探索LLVM如何从优化的IR生成机器指令。\par 4 | 5 | \newpage -------------------------------------------------------------------------------- /content/3/Part-3.tex: -------------------------------------------------------------------------------- 1 | 您将了解如何在LLVM中实现指令选择,并通过添加对新机器指令的支持来应用这些知识。LLVM有一个即时(JIT)编译器,您将了解如何使用它,以及如何根据您的需要对它进行裁剪。您还将尝试各种工具和库,这些工具和库有助于识别应用程序中的错误。最后,将使用一个新的后端扩展LLVM,这将使您掌握利用LLVM尚未支持的新体系结构所需的知识。 \par 2 | 3 | 本节包括以下几章:\par 4 | 5 | \begin{itemize} 6 | \item 第9章,选择指令 7 | \item 第10章,JIT编译 8 | \item 第11章,使用LLVM工具调试 9 | \item 第12章,自定义编译器后端 10 | \end{itemize} 11 | 12 | \newpage -------------------------------------------------------------------------------- /content/3/chapter10/0.tex: -------------------------------------------------------------------------------- 1 | LLVM核心库附带了ExecutionEngine组件,允许在内存中编译和执行IR代码。使用这个组件,我们可以构建即时(JIT)编译器,它允许直接执行IR代码。JIT编译器的工作方式更像解释器,因为不需要将目标代码存储在辅助存储器上。\par 2 | 3 | 在本章中,您将了解JIT编译器的应用程序,以及LLVM JIT编译器的原理。您将探索LLVM动态编译器和解释器,还将学习如何自己实现JIT编译器工具。了解如何使用JIT编译器作为静态编译器的一部分,以及与之相关的挑战。\par 4 | 5 | 本章将包含以下内容:\par 6 | 7 | \begin{itemize} 8 | \item 概述LLVM的JIT实现和用例 9 | \item 使用JIT编译直接执行 10 | \item 利用JIT编译器进行代码计算 11 | \end{itemize} 12 | 13 | 在本章结束时,您将知道如何开发JIT编译器,可以使用预先配置的类,也可以使用符合您需求的定制版本。您还将了解如何在传统静态编译器中使用JIT编译器。\par 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /content/3/chapter10/1.tex: -------------------------------------------------------------------------------- 1 | 本章的代码文件可在\url{https://github.com/PacktPublishing/Learn-LLVM-12/tree/master/Chapter10}获取。\par 2 | 3 | 你可以在视频中找到代码\url{https://bit.ly/3nllhED}。\par -------------------------------------------------------------------------------- /content/3/chapter10/2.tex: -------------------------------------------------------------------------------- 1 | 目前为止,我们只研究了提前(AOT)编译器,这些编译器会编译整个应用程序。只有编译完成后,应用程序才能运行。如果编译是在应用程序的运行时执行的,那么编译器就是一个JIT编译器。JIT编译器有一些有趣的用例:\par 2 | 3 | \begin{itemize} 4 | \item 虚拟机的实现:可以用AOT编译器将编程语言翻译成字节代码。在运行时,使用JIT编译器将字节代码编译为机器码。这种方法的优点是字节码是独立于硬件的,而且由于JIT编译器,与AOT编译器相比,没有性能损失。今天的Java和C\#使用这个模型,但其出现的时间非常久远:1977年的USCD Pascal编译器已经使用了类似的方法。 5 | 6 | \item 表达式解析:电子表格应用程序可以使用JIT编译器编译经常执行的表达式,这可以加速金融模拟。LLVM调试器LLDB就是使用这种方法在调试时计算表达式的。 7 | 8 | \item 数据库查询:数据库从数据库查询创建执行计划。执行计划描述表和列上的操作,这些操作在执行时导致查询答案。可以使用JIT编译器将执行计划转换为机器码,从而加速查询 9 | \end{itemize} 10 | 11 | LLVM的静态编译模型与JIT模型之间的差异并不像您想象的那么大。LLVM静态编译器将LLVM IR编译成机器码,并将结果保存为对象文件。如果目标文件不是存储在磁盘上而是存储在内存中,那么代码是可执行的吗?不能直接执行,因为对全局函数和全局数据的引用使用的是重定位的方式,而不是绝对地址。\par 12 | 13 | 从概念上讲,重定位描述了如何计算地址,例如:作为已知地址的偏移量。如果将重定位解析为地址,就像链接器和动态加载器那样,那么就可以执行目标代码。让静态编译器将IR代码编译成内存中的对象文件,对内存中的对象文件执行链接步骤,然后运行该代码,就得到了一个JIT编译器。LLVM核心库中的JIT实现就是基于这种思想。\par 14 | 15 | 在LLVM的开发历史中,有几种JIT实现,它们具有不同的特性集。最新的JIT API是随请求编译(ORC,on request compilation)引擎。这个缩写有个小故事:在ELF(可执行和链接格式)和DWARF(调试标准)已经存在之后,首席开发人员打算创造另一个基于Tolkien universe的缩写。\par 16 | 17 | ORC引擎构建并扩展了在内存对象文件上使用静态编译器和动态链接器的思想。该实现使用分层的方法,两个基本层次如下:\par 18 | 19 | \begin{enumerate} 20 | \item 编译层 21 | \item 连接层 22 | \end{enumerate} 23 | 24 | 在编译层之上可以有一个提供惰性编译支持的层。转换层可以堆叠在惰性编译层的顶部或下方,允许开发人员添加任意转换,或者只是得到某些事件的通知。这种分层方法优点是,JIT引擎可以针对不同的需求进行定制,例如:高性能虚拟机可能选择预先编译所有内容,而不使用惰性编译层。其他虚拟机将强调启动时间和对用户的响应,并借助惰性编译层实现这一点。\par 25 | 26 | 旧MCJIT引擎仍然可用。这个API派生自一个更古老的、已经删除的JIT引擎。随着时间的推移,这个API变得有点臃肿,而且缺乏ORC API的灵活性。我们的目标是删除这个实现,因为ORC引擎现在可以提供了MCJIT引擎的所有功能。新的项目可以使用ORC API。\par 27 | 28 | 下一节中,我们将讨论lli, LLVM解释器和动态编译器,然后再讨论JIT编译器的实现。\par 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /content/3/chapter10/4.tex: -------------------------------------------------------------------------------- 1 | 编译器编写者为了生成最优代码付出了巨大的努力。一种简单而有效的优化方法是用运算的结果值替换对两个常量的算术运算。为了能够执行计算,嵌入了常量表达式的解释器。为了得到相同的结果,解释器必须实现与生成的机器代码相同的规则!当然,这可能是出错的原因。\par 2 | 3 | 另一种方法是使用相同的代码生成方法将常量表达式编译到IR,然后让JIT编译并执行IR。这个想法甚至可以更进一步。数学中对于相同的输入,函数总是产生相同的结果。对于计算机语言中的函数,这就不一定了。一个例子是rand()函数,它为每次调用返回一个随机值。计算机语言中的函数与数学中的函数具有相同的特性,称为纯函数。在优化表达式期间,我们可以JIT编译和执行纯函数(只有常量参数),并使用JIT执行返回的结果替换对函数的调用。实际上,我们将函数的执行从运行时移动到编译时!\par 4 | 5 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black, title=交叉编译] 6 | 使用JIT编译器作为静态编译器的一部分是一个有趣的选择。但是,如果编译器要支持交叉编译,那么就应该仔细考虑这种方法。引起麻烦的常见候选者是浮点类型,C语言中的长双精度通常取决于硬件和操作系统。有些系统使用128位浮点数,而有些系统只使用64位浮点数。80位浮点类型只在x86平台上可用,通常只在Windows上使用。以不同的精度执行相同的浮点运算可能会导致巨大的差异。在这种情况下不能使用通过JIT编译进行的计算。 7 | \end{tcolorbox} 8 | 9 | 一个函数纯不纯不容易判定,常见的解决方案是应用启发式。如果函数不通过指针或间接使用聚合类型读取或写入堆内存,并且只调用其他纯函数,那么它就是一个纯函数。开发人员可以帮助编译器标记纯函数,例如:用特殊的关键字或符号。在语义分析阶段,编译器可以检查是否存在违规。\par 10 | 11 | 在下一小节中,我们将进一步研究在编译时尝试JIT执行函数时对语义的影响。\par 12 | 13 | \hspace*{\fill} \par %插入空行 14 | \textbf{识别语义} 15 | 16 | 困难的部分是在语义级别,需要决定语言的哪些部分适合在编译时计算。排除对堆内存的访问非常严格,它排除了字符串处理。当分配的内存在JIT执行函数的生命周期内仍然存在时,使用堆内存就会出现问题。这是一种程序状态,可能会影响其他结果。另一方面,如果有对malloc()和free()函数的匹配调用,那么内存只用于内部计算。在这种情况下,使用堆内存是安全的。但确切地说,这并不容易证明。\par 17 | 18 | 在类似的级别上,JIT执行函数内部的无限循环可能会冻结编译器。1936年,艾伦·图灵(Alan Turing)证明,没有机器可以决定一个函数是否会产生结果,或者是否陷入了无休止的循环。必须采取一些预防措施来避免这种情况,例如:在运行时限制之后,JIT执行的函数需要停止。\par 19 | 20 | 最后,允许的功能越多,就必须更多地考虑安全性,因为编译器现在执行别人编写的代码。想象一下,这些代码从互联网上下载并运行文件,或者试图擦除硬盘:对于JIT执行的函数,允许的状态太多了,因此我们也需要考虑这样的场景。\par 21 | 22 | 这个想法并不新。D编程语言有一个叫做编译时执行函数的特性。引用编译器dmd通过在AST级别解释函数来实现这个特性。基于LLVM的LDC编译器实验性的特性,可以为它使用LLVM JIT引擎。可以在\url{https://dlang.org/}找到更多关于语言和编译器的信息。\par 23 | 24 | 忽略语义挑战,实现并不是那么困难。在从零开始构建JIT编译器类的章节中,我们使用JIT类开发了一个JIT编译器。在类中输入一个IR模块,然后可以从这个模块查找并执行函数。看看tinylang编译器的实现,我们可以清楚地识别对常量的访问,因为AST中有一个ConstantAccess节点。例如:\par 25 | 26 | \begin{lstlisting}[caption={}] 27 | if (auto *Const = llvm::dyn_cast(Expr)) { 28 | // Do something with the constant. 29 | } 30 | \end{lstlisting} 31 | 32 | 不需要解释表达式中的运算来推导常量的值,我们可以做以下操作:\par 33 | 34 | \begin{enumerate} 35 | \item 创建一个新的IR模块。 36 | 37 | \item 在模块中创建一个IR函数,返回一个预期类型的值。 38 | 39 | \item 使用现有的emitExpr()函数为表达式创建IR,并使用最后一条指令返回计算值。 40 | 41 | \item JIT执行函数来计算值。 42 | \end{enumerate} 43 | 44 | 这是否值得实现?作为优化流水的一部分,LLVM执行常量传播和函数内联。像4+5这样的简单表达式在构建IR时已经替换了,像计算最大公约数这样的小函数是内联的。如果所有参数都是常量值,那么内联代码将通过常量传播。\par 45 | 46 | 根据这一观察,只有在有足够的语言特性可在编译时执行时,这种方法的实现才有用。如果是这样,那么使用给定的示意图就相当容易实现了。\par 47 | 48 | 了解如何使用LLVM的JIT编译器组件使您能够以全新的方式使用LLVM。除了实现像Java VM这样的JIT编译器外,JIT编译器还可以嵌入到其他应用程序中。这允许使用创造性的方法,例如:在静态编译器中使用它(这一节中已经介绍过了)。\par 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /content/3/chapter10/5.tex: -------------------------------------------------------------------------------- 1 | 本章中,您学习了如何开发JIT编译器。从JIT编译器应用开始,探索了LLVM动态编译器和解释器lli。使用预定义的LLJIT类,您可以自己构建一个类似于lli的工具。为了能够利用ORC API的分层结构,实现了一个优化JIT类。掌握了所有这些知识后,了解了在静态编译器中使用JIT编译器的可能性,有一些语言可以从中受益。\par 2 | 3 | 下一章中,您将了解如何为LLVM添加一个新CPU体系架构的后端。\par 4 | 5 | \newpage -------------------------------------------------------------------------------- /content/3/chapter11/0.tex: -------------------------------------------------------------------------------- 1 | LLVM附带了一组工具,可以帮助您查找应用程序中的某些错误。所有这些工具都会使用LLVM和Clang的库。\par 2 | 3 | 本章中,您将学习如何使用sanitizer来检测应用程序,如何使用最常见的sanitizer来识别各种各样的错误,以及如何为您的应用程序实现模糊测试(fuzz testing)。这将帮助您识别单元测试通常无法发现的bug。您还将学习如何在应用程序中识别性能瓶颈,运行静态分析程序来识别编译器通常没有发现的问题,以及创建您自己的(基于Clang)的工具,您可以使用它来扩展Clang的新功能。\par 4 | 5 | 本章将涵盖以下内容:\par 6 | 7 | \begin{itemize} 8 | \item 使用sanitizer检查应用程序 9 | \item 用libFuzzer找到bug 10 | \item 使用xRay进行性能分析 11 | \item 使用Clang静态分析器检查源代码 12 | \item 创建基于Clang的工具 13 | \end{itemize} 14 | 15 | 本章结束时,您将了解如何使用各种LLVM和Clang工具来识别应用程序中的错误。您还将获得使用新功能扩展Clang的知识,例如:执行命名约定或添加新的源码分析工具。\par 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /content/3/chapter11/1.tex: -------------------------------------------------------------------------------- 1 | 要在使用XRay的性能分析部分创建火焰图,需要从\url{https://github.com/brendangregg/FlameGraph}安装脚本。有些系统,如Fedora和FreeBSD,为这些脚本提供了安装包,可以直接使用安装包安装。\par 2 | 3 | 要查看Chrome可视化,需要安装Chrome浏览器。可以从\url{https://www.google.com/chrome/}下载浏览器,或者使用系统的包管理器来安装Chrome浏览器。本章的代码文件可以在\url{https://github.com/PacktPublishing/Learn-LLVM-12/tree/master/Chapter11}获取。\par 4 | 5 | 可以在视频中找到代码\url{https://bit.ly/3nllhED}。\par -------------------------------------------------------------------------------- /content/3/chapter11/2.tex: -------------------------------------------------------------------------------- 1 | 2 | LLVM自带一些sanitizer。这些Pass以某种方式检测中间表示(IR),以检查应用程序的某些不当行为。通常,需要库支持,这是compiler-rt项目的一部分。可以在Clang中启用sanitizer,这让它们使用起来更加方便。下面的小节中,我们将介绍可用的sanitizer,即地址、内存和线程。我们先来看看地址sanitizer。\par 3 | 4 | \hspace*{\fill} \par %插入空行 5 | \textbf{用地址sanitizer检测内存访问问题} 6 | 7 | 可以使用地址sanitizer来检测应用程序中的两个内存访问错误。这包括一些常见的错误,比如:在释放动态分配的内存后使用它,或者在已分配内存的边界之外写入动态分配的内存。\par 8 | 9 | 当启用地址sanitizer时,地址sanitizer将用它自己的版本替换对malloc()和free()函数的调用,并使用检查保护程序检测所有内存访问。当然,这给应用程序增加了很多开销,您将只在应用程序的测试阶段使用地址消毒剂。如果对实现细节感兴趣,可以在llvm/lib/Transforms/Instrumentation/\allowbreak AddressSanitzer.cpp文件中找到Pass源,并在\url{https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm}中找到算法描述。\par 10 | 11 | 让我们运行一个简短的示例来演示地址sanitizer的功能。下面的示例应用程序outfbounds.c分配了12字节的内存,但初始化了14字节:\par 12 | 13 | \begin{lstlisting}[caption={}] 14 | #include 15 | #include 16 | 17 | int main(int argc, char *argv[]) { 18 | char *p = malloc(12); 19 | memset(p, 0, 14); 20 | return (int)*p; 21 | } 22 | \end{lstlisting} 23 | 24 | 您可以编译并运行此应用程序,而不会注意到任何问题。这是这中错误的典型特征。即使在较大的应用程序中,这种错误也可能在很长一段时间内不被注意。但是,如果使用-fsanitize=address选项启用地址sanitizer,那么应用程序在检测到错误后将停止。\par 25 | 26 | 使用-g选项启用调试符号也很有用,因为它有助于识别源文件中错误的位置。下面的代码示例说明了如何在启用地址消毒剂和调试符号的情况下编译源文件:\par 27 | 28 | \begin{tcolorbox}[colback=white,colframe=black] 29 | \$ clang -fsanitize=address -g outofbounds.c -o outofbounds 30 | \end{tcolorbox} 31 | 32 | 现在,当运行应用程序时,会得到一个冗长的错误报告:\par 33 | 34 | \begin{tcolorbox}[colback=white,colframe=black] 35 | \$ ./outofbounds \\ 36 | ============================================= \\ 37 | === \\ 38 | ==1067==ERROR: AddressSanitizer: heap-buffer-overflow on \\ 39 | address 0x60200000001c at pc 0x00000023a6ef bp 0x7fffffffeb10 \\ 40 | sp 0x7fffffffe2d8 \\ 41 | WRITE of size 14 at 0x60200000001c thread T0 \\ 42 | \hspace*{1cm}\#0 0x23a6ee in \underline{~~}asan\underline{~}memset /usr/src/contrib/llvm-project/ \\ 43 | compiler-rt/lib/asan/asan\underline{~}interceptors\underline{~}memintrinsics.cpp:26:3 \\ 44 | \hspace*{1cm}\#1 0x2b2a03 in main /home/kai/sanitizers/outofbounds.c:6:3 \\ 45 | \hspace*{1cm}\#2 0x23331f in \underline{~}start /usr/src/lib/csu/amd64/crt1.c:76:7 46 | \end{tcolorbox} 47 | 48 | 报告还包含关于内存内容的详细信息。重要的信息是错误的类型(在本例中是堆缓冲区溢出)和出错的源行。要找到源码行,可以查看位置\#1的堆栈跟踪,这是地址sanitizer拦截应用程序执行之前的最后一个位置。它显示了outfbounds.c文件中的第6行,这一行包含了对memset()的调用——实际上,这就是发生内存溢出的位置。\par 49 | 50 | 替换包含memset(p, 0, 14);在outfbounds.c文件中使用以下代码,然后在释放内存后引入对内存的访问。并将源代码存储在useafterfree.c中:\par 51 | 52 | \begin{lstlisting}[caption={}] 53 | memset(p, 0, 12); 54 | free(p); 55 | \end{lstlisting} 56 | 57 | 同样,如果编译并运行,会检测到内存释放后指针的使用情况:\par 58 | 59 | \begin{tcolorbox}[colback=white,colframe=black] 60 | \$ clang -fsanitize=address -g useafterfree.c -o useafterfree \\ 61 | \$ ./useafterfree \\ 62 | ============================================== \\ 63 | === \\ 64 | ==1118==ERROR: AddressSanitizer: heap-use-after-free on address \\ 65 | 0x602000000010 at pc 0x0000002b2a5c bp 0x7fffffffeb00 sp \\ 66 | 0x7fffffffeaf8 \\ 67 | READ of size 1 at 0x602000000010 thread T0 \\ 68 | \hspace*{1cm}\#0 0x2b2a5b in main /home/kai/sanitizers/ \\ 69 | useafterfree.c:8:15 \\ 70 | \hspace*{1cm}\#1 0x23331f in \underline{~}start /usr/src/lib/csu/amd64/crt1.c:76:7 71 | \end{tcolorbox} 72 | 73 | 这一次,报告指向第8行,其中包含p指针的释放。\par 74 | 75 | 在x86\underline{~}64 Linux和macOS上,也可以启用泄漏检测器。如果在运行应用程序之前将ASAN\underline{~}OPTIONS环境变量设置为detect\underline{~}leaks=1,还会得到一个关于内存泄漏的报告。可以这样做:\par 76 | 77 | \begin{tcolorbox}[colback=white,colframe=black] 78 | \$ ASAN\underline{~}OPTIONS=detect\underline{~}leaks=1 ./useafterfree 79 | \end{tcolorbox} 80 | 81 | 地址sanitizer非常有用,因为它捕获了一类用其他方法很难检测到的bug。内存sanitizer执行类似的任务,我们将在下一节中研究它。\par 82 | 83 | \hspace*{\fill} \par %插入空行 84 | \textbf{使用内存sanitizer查找未初始化的内存访问} 85 | 86 | 使用未初始化的内存是另一类难以发现的错误。在C和C++中,一般的内存分配例程不会用默认值初始化内存缓冲区,对于堆栈上的变量也是如此。\par 87 | 88 | 出现错误的机会很多,而内存sanitizer有助于找到错误。如果对实现细节感兴趣,可以在llvm/lib/Transforms/Instrumentation/MemorySanitizer.cpp中找到内存sanitizer Pass的源文件。文件顶部的注释解释了实现思想。\par 89 | 90 | 让我们运行一个小示例,并将下面的源代码保存为memory.c文件。你应该注意到x变量没有初始化,而是用作返回值:\par 91 | 92 | \begin{lstlisting}[caption={}] 93 | int main(int argc, char *argv[]) { 94 | int x; 95 | return x; 96 | } 97 | \end{lstlisting} 98 | 99 | 没有sanitizer,应用程序将运行得很好。如果你使用-fsanitize=memory选项,就会得到一个错误报告:\par 100 | 101 | \begin{tcolorbox}[colback=white,colframe=black] 102 | \$ clang -fsanitize=memory -g memory.c -o memory \\ 103 | \$ ./memory \\ 104 | ==1206==WARNING: MemorySanitizer: use-of-uninitialized-value \\ 105 | \hspace*{1cm}\#0 0x10a8f49 in main /home/kai/sanitizers/memory.c:3:3 \\ 106 | \hspace*{1cm}\#1 0x1053481 in \underline{~}start /usr/src/lib/csu/amd64/crt1.c:76:7 \\ 107 | \\ 108 | SUMMARY: MemorySanitizer: use-of-uninitialized-value /home/kai/ \\ 109 | sanitizers/memory.c:3:3 in main \\ 110 | Exiting 111 | \end{tcolorbox} 112 | 113 | 与地址sanitizer一样,内存sanitizer在发现第一个错误时停止应用程序。\par 114 | 115 | 下一节中,我们将了解如何使用线程sanitizer来检测多线程应用程序中的数据竞争。\par 116 | 117 | \hspace*{\fill} \par %插入空行 118 | \textbf{用线程sanitizer指出数据竞争} 119 | 120 | 为了充分利用现代CPU的功能,应用程序现在使用多线程。这是一项强大的技术,但它也引入了新的错误来源。多线程应用程序中一个常见的问题是,对全局数据的访问没有保护,例如:没有使用互斥锁或信号量。这样的问题称为数据竞争。线程sanitizer可以检测基于pthread的应用程序和使用LLVM libc++实现的应用程序中的数据竞争。可以在llvm/lib/Transforms/Instrumentation/\allowbreak ThreadSanitize.cpp文件中找到实现。\par 121 | 122 | 为了演示线程sanitizer的功能,我们将创建一个简单的生产者/消费者的应用程序。生产者线程增加一个全局变量,而消费者线程减少同一个变量。对全局变量的访问不受保护,因此这显然是一场数据竞争。在thread.c文件中保存以下源代码:\par 123 | 124 | \begin{lstlisting}[caption={}] 125 | #include 126 | 127 | int data = 0; 128 | 129 | void *producer(void *x) { 130 | for (int i = 0; i < 10000; ++i) ++data; 131 | return x; 132 | } 133 | 134 | void *consumer(void *x) { 135 | for (int i = 0; i < 10000; ++i) --data; 136 | return x; 137 | } 138 | 139 | int main() { 140 | pthread_t t1, t2; 141 | pthread_create(&t1, NULL, producer, NULL); 142 | pthread_create(&t2, NULL, consumer, NULL); 143 | pthread_join(t1, NULL); 144 | pthread_join(t2, NULL); 145 | return data; 146 | } 147 | \end{lstlisting} 148 | 149 | 前面的代码中,数据变量在两个线程之间共享。这里,它是int类型,以使示例简单。最常见的情况是使用std::vector类或类似的数据结构。这两个线程运行producer()和consumer()函数。\par 150 | 151 | producer()函数只增加数据变量,而consumer()函数减少数据变量。没有实现访问保护,因此这构成了一场数据竞争。main()函数使用pthread\underline{~}create()函数启动两个线程,使用pthread\underline{~}join()函数等待线程结束,并返回数据变量的当前值。\par 152 | 153 | 如果编译并运行此应用程序,则不会注意到任何错误,返回值总是0。如果执行的循环次数增加100倍,则会出现一个错误。本例中,返回值不等于0,您将看到显示其他值。\par 154 | 155 | 您可以使用线程sanitizer来标识数据竞争。要在启用了线程sanitizer的情况下进行编译,需要将-fsanitize=thread选项传递给Clang。使用-g选项添加调试符号可以在报告中提供行号,这很有帮助。注意,还需要链接pthread库:\par 156 | 157 | \begin{tcolorbox}[colback=white,colframe=black] 158 | \$ clang -fsanitize=thread -g thread.c -o thread -lpthread \\ 159 | \$ ./thread \\ 160 | ================== \\ 161 | WARNING: ThreadSanitizer: data race (pid=1474) \\ 162 | \hspace*{0.5cm}Write of size 4 at 0x000000cdf8f8 by thread T2: \\ 163 | \hspace*{1cm}\#0 consumer /home/kai/sanitizers/thread.c:11:35 164 | (thread+0x2b0fb2) \\ 165 | \\ 166 | \hspace*{0.5cm}Previous write of size 4 at 0x000000cdf8f8 by thread T1: \\ 167 | \hspace*{1cm}\#0 producer /home/kai/sanitizers/thread.c:6:35 168 | (thread+0x2b0f22) 169 | \\ 170 | \hspace*{0.5cm}Location is global 'data' of size 4 at 0x000000cdf8f8 171 | (thread+0x000000cdf8f8) 172 | \\ 173 | \hspace*{0.5cm}Thread T2 (tid=100437, running) created by main thread at: \\ 174 | \hspace*{1cm}\#0 pthread\underline{~}create /usr/src/contrib/llvm-project/ \\ 175 | compiler-rt/lib/tsan/rtl/tsan\underline{~}interceptors\underline{~}posix.cpp:962:3 176 | (thread+0x271703) \\ 177 | \hspace*{1cm}\#1 main /home/kai/sanitizers/thread.c:18:3 178 | (thread+0x2b1040) \\ 179 | \\ 180 | \hspace*{0.5cm}Thread T1 (tid=100436, finished) created by main thread at: \\ 181 | \hspace*{1cm}\#0 pthread\underline{~}create /usr/src/contrib/llvm-project/ \\ 182 | compiler-rt/lib/tsan/rtl/tsan\underline{~}interceptors\underline{~}posix.cpp:962:3 183 | (thread+0x271703) \\ 184 | \\ 185 | \hspace*{1cm}\#1 main /home/kai/sanitizers/thread.c:17:3 186 | (thread+0x2b1021) \\ 187 | \\ 188 | SUMMARY: ThreadSanitizer: data race /home/kai/sanitizers/ 189 | thread.c:11:35 in consumer \\ 190 | ================== \\ 191 | ThreadSanitizer: reported 1 warnings 192 | \end{tcolorbox} 193 | 194 | 报告指向源文件的第6行和第11行,其中访问全局变量。它还显示了两个名为T1和T2的线程访问了该变量,以及分别调用pthread\underline{~}create()函数的文件和行号。\par 195 | 196 | 本节中,我们学习了如何使用三种sanitizer来查找应用程序中的常见问题。地址sanitizer帮助我们识别常见的内存访问错误,例如:越界访问或在释放后使用内存。使用内存sanitizer,可以找到对未初始化内存的访问,线程sanitizer帮助我们查找数据竞争。\par 197 | 198 | 下一节中,我们将尝试通过在随机数据上运行应用程序(称为模糊测试)来触发sanitizer。\par 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /content/3/chapter11/3.tex: -------------------------------------------------------------------------------- 1 | 2 | 要测试应用程序,需要编写单元测试。这是确保软件正常运行的好方法。然而,可能的输入呈指数级增长,很可能会错过某些奇怪的输入,以及一些bug。\par 3 | 4 | 模糊测试可以在这里提供帮助。其思想是为应用程序提供随机生成的数据,或基于有效输入但随机更改的数据。这是一遍又一遍地进行的,因此您的应用程序将使用大量输入进行测试。这是一种非常强大的测试方法。通过模糊测试,发现了网络浏览器和其他软件中的数百个漏洞。\par 5 | 6 | LLVM自带了自己的模糊测试库。libFuzzer实现最初是LLVM核心库的一部分,后来移到了compiler-rt上。该库旨在测试小而快速的函数。\par 7 | 8 | 让我们运行一个小示例。需要提供LLVMFuzzerTestOneInput()函数。这个函数由fuzzer驱动程序调用,并为您提供一些输入。下面的函数计算输入中的连续ASCII数字,然后我们将随机输入输入给它。需要将示例保存在fuzzer.c文件中:\par 9 | 10 | \begin{lstlisting}[caption={}] 11 | #include 12 | #include 13 | 14 | int count(const uint8_t *Data, size_t Size) { 15 | int cnt = 0; 16 | if (Size) 17 | while (Data[cnt] >= '0' && Data[cnt] <= '9') ++cnt; 18 | return cnt; 19 | } 20 | 21 | int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t 22 | Size) { 23 | count(Data, Size); 24 | return 0; 25 | } 26 | \end{lstlisting} 27 | 28 | 代码中,count()函数计算Data变量所指向的内存中的位数。只检查数据的大小以确定是否有可用的字节。在while循环中,不检查数据长度。\par 29 | 30 | 与普通C字符串一起使用时,不会出现错误,因为C字符串总是以0字节结束。LLVMFuzzer\allowbreak TestOneInput()函数就是fuzz目标,它是libFuzzer调用的函数。调用我们想要测试的函数并返回0,这是目前唯一允许的值。\par 31 | 32 | 要使用libFuzzer编译文件,需要添加-fsanitize=fuzzer选项。建议还启用地址sanitizer和调试符号的生成:\par 33 | 34 | \begin{tcolorbox}[colback=white,colframe=black] 35 | \$ clang -fsanitize=fuzzer,address -g fuzzer.c -o fuzzer 36 | \end{tcolorbox} 37 | 38 | 当您运行测试时,会产生一个冗长的报告。报告包含比堆栈跟踪更多的信息,所以让我们仔细来看看:\par 39 | 40 | \begin{enumerate} 41 | \item 第一行告诉您用于初始化随机数生成器的种子。可以使用-seed=来重复执行: 42 | \begin{tcolorbox}[colback=white,colframe=black] 43 | INFO: Seed: 1297394926 44 | \end{tcolorbox} 45 | 46 | \item 默认情况下,libFuzzer将输入限制为最多4,096字节。可以使用-max \underline{~}len=来更改默认值: 47 | \begin{tcolorbox}[colback=white,colframe=black] 48 | INFO: -max\underline{~}len is not provided; libFuzzer will not \\ 49 | generate inputs larger than 4096 bytes 50 | \end{tcolorbox} 51 | 52 | \item 现在,在不提供示例输入的情况下运行测试。所有样本输入的集合称为语料库,在这次运行中它是空的: 53 | \begin{tcolorbox}[colback=white,colframe=black] 54 | INFO: A corpus is not provided, starting from an empty corpus 55 | \end{tcolorbox} 56 | 57 | \item 下面是一些关于生成的测试数据的信息。尝试了28个输入,找到了6个输入,总长度为19字节,总共覆盖了6个点或基本块: 58 | \begin{tcolorbox}[colback=white,colframe=black] 59 | \#28 NEW cov: 6 ft: 9 corp: 6/19b lim: 4 exec/s: 0 \\ 60 | rss: 29Mb L: 4/4 MS: 4 CopyPart-PersAutoDict-CopyPart- \\ 61 | ChangeByte- DE: "1$\setminus$x00"- 62 | \end{tcolorbox} 63 | 64 | \item 之后,检测到内存溢出,然后就是地址sanitizer的信息。最后,报告显示导致内存溢出的输入在哪里: 65 | \begin{tcolorbox}[colback=white,colframe=black] 66 | artifact\underline{~}prefix='./'; Test unit written to ./crash-17ba0791499db908433b80f37c5fbc89b87\allowbreak 0084b 67 | \end{tcolorbox} 68 | 69 | \end{enumerate} 70 | 71 | 保存了输入,就可以用崩溃的输入再次执行测试用例:\par 72 | 73 | \begin{tcolorbox}[colback=white,colframe=black] 74 | \$ ./fuzzer crash-17ba0791499db908433b80f37c5fbc89b870084b 75 | \end{tcolorbox} 76 | 77 | 这显然对识别问题有很大帮助。只是,使用随机数据通常不是很有帮助。如果您尝试对tinylang词法分析器或解析器进行模糊测试,因为无法找到有效的标记,所以纯随机数据将导致输入立即被拒绝。\par 78 | 79 | 这种情况下,提供一小组称为语料库的有效输入更有用。然后,对语料库中的文件进行随机变异并作为输入。您可以认为输入基本上是有效的,只翻转了几个位。这也适用于其他必须具有特定格式的输入,例如:对于处理JPEG和PNG文件的库,可以将提供一些小的JPEG和PNG文件作为语料库。\par 80 | 81 | 您可以将语料库文件保存在一个或多个目录中,并可以在printf命令的帮助下为fuzz测试创建一个简单的语料库:\par 82 | 83 | \begin{tcolorbox}[colback=white,colframe=black] 84 | \$ mkdir corpus \\ 85 | \$ printf "012345$\setminus$0" >corpus/12345.txt \\ 86 | \$ printf "987$\setminus$0" >corpus/987.txt 87 | \end{tcolorbox} 88 | 89 | 当运行测试时,可以在命令行上提供语料库目录:\par 90 | 91 | \begin{tcolorbox}[colback=white,colframe=black] 92 | \$ ./fuzzer corpus/ 93 | \end{tcolorbox} 94 | 95 | 然后使用语料库作为生成随机输入的基础,如报告所示:\par 96 | 97 | \begin{tcolorbox}[colback=white,colframe=black] 98 | INFO: seed corpus: files: 2 min: 4b max: 7b total: 11b rss: 29Mb 99 | \end{tcolorbox} 100 | 101 | 如果您正在测试一个作用于令牌或其他魔数值(如编程语言)的函数,那么可以通过提供一个带有令牌的字典来加快这个过程。对于编程语言,字典将包含该语言中使用的所有关键字和特殊符号。字典定义遵循简单的键-值样式,例如:要在字典中定义if关键字,可以添加以下内容:\par 102 | 103 | \begin{tcolorbox}[colback=white,colframe=black] 104 | kw1="if" 105 | \end{tcolorbox} 106 | 107 | 但是,这个键是可选的,可以省略。然后可以使用-dict=在命令行上指定字典文件。下一节中,我们将了解libFuzzer实现的限制和替代方案。\par 108 | 109 | \hspace*{\fill} \par %插入空行 110 | \textbf{限制和替代} 111 | 112 | libFuzzer实现速度很快,但对测试目标有许多限制:\par 113 | 114 | 115 | \begin{itemize} 116 | \item 测试函数必须在内存中以数组的形式接受输入。有些库函数需要数据的文件路径,不能用libFuzzer测试它们。 117 | 118 | \item 不应该调用exit()函数。 119 | 120 | \item 全局状态不应改变。 121 | 122 | \item 不应该使用硬件随机数生成器。 123 | \end{itemize} 124 | 125 | 从前面提到的限制中可以看出,前两个限制是libFuzzer作为库实现的一个含义。在后两个限制是必需的,以避免在评估算法中的混淆。如果没有满足其中一个限制,那么对模糊目标的两次相同调用可能会得到不同的结果。\par 126 | 127 | 最著名的模糊测试替代工具是AFL,可以在\url{https://github.com/google/AFL}找到。AFL需要一个检测的二进制文件(提供了一个用于检测的LLVM插件),并要求应用程序将输入作为命令行上的文件路径。AFL和libFuzzer可以共享相同的语料库和字典文件。因此,可以使用这两种工具来测试应用程序。在libFuzzer不适用的情况下,AFL是一个很好的替代方案。\par 128 | 129 | 有很多方法可以影响libFuzzer的工作方式,可以在\url{https://llvm.org/docs/LibFuzzer.html}了解更多细节。\par 130 | 131 | 下一节中,我们将讨论应用程序可能存在的一个完全不同的问题,试图查找性能瓶颈。\par 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /content/3/chapter11/4.tex: -------------------------------------------------------------------------------- 1 | 2 | 如果应用程序运行缓慢,那么很可能想知道代码中的时间都花费在什么地方了。在这种情况下,使用XRay检测代码会有所帮助。基本上,在每个函数的入口和出口,都会插入一个对运行时库的特殊调用。这允许计算函数被调用的频率,以及在函数中花费的时间。可以在llvm/lib/XRay/目录中找到Pass的实现,运行时是compiler-rt的一部分。\par 3 | 4 | 下面的示例源代码中,通过调用usleep()函数来模拟实际工作。函数的作用是:休眠10μs。func2()函数要么调用func1(),要么休眠100μs,这取决于n参数是奇数还是偶数。在main()函数中,两个函数都是在循环中调用的。在xraydemo.c文件中保存以下代码:\par 5 | 6 | \begin{lstlisting}[caption={}] 7 | #include 8 | 9 | void func1() { usleep(10); } 10 | 11 | void func2(int n) { 12 | if (n % 2) func1(); 13 | else usleep(100); 14 | } 15 | 16 | int main(int argc, char *argv[]) { 17 | for (int i = 0; i < 100; i++) { func1(); func2(i); } 18 | return 0; 19 | } 20 | \end{lstlisting} 21 | 22 | 要在编译期间启用XRay检测,需要指定-fxray-instrument选项,不测试小于200条指令的函数。这是一个由开发人员定义的任意阈值,在例子中,函数不会被检测。该阈值可以通过-fxrayin\allowbreak struction-threshold= 指定,或者可以添加一个function属性来控制函数是否应该检测,例如:添加以下修饰会让函数始终执行检测:\par 23 | 24 | \begin{lstlisting}[caption={}] 25 | void func1() __attribute__((xray_always_instrument)); 26 | \end{lstlisting} 27 | 28 | 同样,通过使用xray\underline{~}never\underline{~}instrument属性,可以关闭函数的检测功能。\par 29 | 30 | 现在,将使用命令行选项编译xraydemo.c文件: 31 | 32 | \begin{tcolorbox}[colback=white,colframe=black] 33 | \$ clang -fxray-instrument -fxray-instruction-threshold=1 -g $\setminus$ \\ 34 | \hspace*{1cm}xraydemo.c -o xraydemo 35 | \end{tcolorbox} 36 | 37 | 现在,将使用命令行选项编译xraydemo.c文件在生成的二进制文件中,检测功能默认关闭。如果运行该二进制文件,您将不会注意到与未插装的二进制文件的区别。XRA\underline{~}OPTIONS环境变量用于控制运行时数据的记录。要启用数据收集,运行应用程序如下所示:\par 38 | 39 | \begin{tcolorbox}[colback=white,colframe=black] 40 | \$ XRAY\underline{~}OPTIONS= "patch\underline{~}premain=true xray\underline{~}mode=xray-basic "$\setminus$ \\ 41 | ./xraydemo 42 | \end{tcolorbox} 43 | 44 | xray\underline{~}mode=xray-basic选项告诉运行时我们想要使用基本模式。在这种模式下,将收集所有的运行时数据,这可能生成巨大的日志文件。当给出patch\underline{~}premain=true选项时,那么在main()函数之前运行的函数也会被检测。\par 45 | 46 | 执行该命令后,将在收集的数据所在的目录中看到一个新文件,需要使用llvm-xray工具从这个文件中提取可读的信息。\par 47 | 48 | llvm-xray工具支持各种子命令。可以使用account子命令提取一些基本统计信息,例如:要获得被调用最多的前10个函数,可以添加-top=10选项来限制输出,并添加-sort=count选项来指定函数调用计数作为排序标准。可以使用-sortorder=选项影响排序顺序。执行如下命令获取统计信息:\par 49 | 50 | \begin{tcolorbox}[colback=white,colframe=black] 51 | \$ llvm-xray account xray-log.xraydemo.xVsWiE -sort=count$\setminus$ \\ 52 | \hspace*{0.5cm}-sortorder=dsc -instr\underline{~}map ./xraydemo \\ 53 | Functions with latencies: 3 \\ 54 | \hspace*{0.5cm}funcid \hspace{1cm}count \hspace{2cm}sum \hspace{1cm}function \\ 55 | \hspace*{1.4cm}1\hspace{1.4cm}150\hspace{1.4cm}0.166002 \hspace{1cm}demo.c:4:0: func1 \\ 56 | \hspace*{1.4cm}2\hspace{1.4cm}100\hspace{1.4cm}0.543103 \hspace{1cm}demo.c:9:0: func2 \\ 57 | \hspace*{1.4cm}3\hspace{1.8cm}1 \hspace{1.3cm}0.655643 \hspace{1cm}demo.c:17:0: main 58 | \end{tcolorbox} 59 | 60 | 可以看到,func1()函数调用得最频繁,以及在这个函数中花费的累计时间。这个示例只有三个函数,所以-top=在这里没有明显的效果,但对于实际应用程序来说,它非常有用。\par 61 | 62 | 从收集的数据中,可以重构运行时发生的所有堆栈帧。可以使用stack子命令查看排名前10的堆叠。为了简洁起见,此处显示的输出简化了:\par 63 | 64 | \begin{tcolorbox}[colback=white,colframe=black] 65 | \$ llvm-xray stack xray-log.xraydemo.xVsWiE -instr\underline{~}map$\setminus$ \\ 66 | \hspace*{0.5cm}./xraydemo \\ 67 | Unique Stacks: 3 \\ 68 | Top 10 Stacks by leaf sum:\\ 69 | \\ 70 | Sum: 1325516912\\ 71 | lvl \hspace{1cm}function \hspace{2.5cm}count \hspace{2.5cm}sum\\ 72 | \#0\hspace{1.5cm}main \hspace{3.3cm}1 \hspace{1.3cm}1777862705 \\ 73 | \#1\hspace{1.5cm}func2 \hspace{3.0cm}50 \hspace{1.35cm}1325516912 \\ 74 | \\ 75 | Top 10 Stacks by leaf count:\\ 76 | \\ 77 | Count: 100 \\ 78 | lvl \hspace{1cm}function \hspace{2.5cm}count \hspace{2.5cm}sum \\ 79 | \#0\hspace{1.5cm}main \hspace{3.3cm}1 \hspace{1.3cm}1777862705 \\ 80 | \#1\hspace{1.5cm}func1 \hspace{2.9cm}100 \hspace{1.45cm}303596276 81 | \end{tcolorbox} 82 | 83 | 堆栈帧是一个函数调用的序列。func2()函数由main()函数调用,这是累积耗时最长的堆栈帧。深度取决于调用多少函数,堆栈帧通常很大。\par 84 | 85 | 这个子命令还可以用于从堆栈帧创建火焰图,可以很容易地识别哪些函数具有较大的累积运行时。输出是包含计数和运行时信息的堆栈帧。使用flamegraph.pl脚本,可以将数据转换为可缩放矢量图形(Scalable Vector Graphics, SVG)文件,并且可以在浏览器中查看该文件。\par 86 | 87 | 使用下面的命令,可以指示llvm-xray使用-all-stacks选项输出所有堆栈帧。使用-stack-format=\allowbreak flame选项,输出将以flamegraph.pl脚本所期望的格式显示。使用-aggregationtype选项,可以选择是按总时间还是按调用次数聚合堆栈帧。llvm-xray的输出通过管道传输到flamegraph.pl脚本中,结果输出保存在flame.svg文件中:\par 88 | 89 | \begin{tcolorbox}[colback=white,colframe=black] 90 | \$ llvm-xray stack xray-log.xraydemo.xVsWiE -all-stacks$\setminus$ \\ 91 | \hspace*{0.5cm}-stack-format=flame \verb|--|aggregation-type=time$\setminus$ \\ 92 | \hspace*{0.5cm}-instr\underline{~}map ./xraydemo | flamegraph.pl >flame.svg 93 | \end{tcolorbox} 94 | 95 | 在浏览器中打开生成的flame.svg文件。图表如下所示:\par 96 | 97 | \hspace*{\fill} \par %插入空行 98 | \begin{center} 99 | \includegraphics[width=1\textwidth]{content/3/chapter11/images/1.jpg}\\ 100 | 图11.1 – 由llvm-x射线生成的火焰图 101 | \end{center} 102 | 103 | 火焰图第一眼看起来可能会令人困惑,因为x轴没有通常的流逝时间的含义。相反,函数只是简单地按名称排序。颜色的选择要有良好的对比,没有其他意义。从上面的图中,可以很容易地确定调用层次结构和在函数中花费的时间。\par 104 | 105 | 只有将鼠标移到表示堆栈帧的矩形上,才会显示堆栈帧的相关信息。用鼠标单击框架,可以放大此堆栈框架。如果想要确定值得优化的函数,火焰图是很有帮助的。想了解更多关于火焰图的信息,请访问火焰图的作者Brendan Gregg的网站,\url{http://www.brendangregg.com/flamegraphs.html}。\par 106 | 107 | 您可以使用convert子命令将数据转换为.yaml格式或Chrome跟踪查看器可视化使用的格式。后者是另一种从数据创建图形的好方法。将数据保存在xray.evt文件中:\par 108 | 109 | \begin{tcolorbox}[colback=white,colframe=black] 110 | \$ llvm-xray convert -output-format=trace\underline{~}event$\setminus$ \\ 111 | \hspace*{0.5cm}-output=xray.evt -symbolize –sort$\setminus$ \\ 112 | \hspace*{0.5cm}-instr\underline{~}map=./xraydemo xray-log.xraydemo.xVsWiE 113 | \end{tcolorbox} 114 | 115 | 如果不指定-symbolic选项,则结果图中不会显示函数名。\par 116 | 117 | 一旦完成,打开Chrome浏览器,输入Chrome:///跟踪。然后,点击加载按钮来加载xray.evt文件。您将看到以下可视化数据:\par 118 | 119 | \hspace*{\fill} \par %插入空行 120 | \begin{center} 121 | \includegraphics[width=1\textwidth]{content/3/chapter11/images/2.jpg}\\ 122 | 图11.2 – Chrome跟踪查看器可视化生成的llvm-xray 123 | \end{center} 124 | 125 | 在这个视图中,堆栈帧按函数调用发生的时间排序。关于可视化的进一步解释,请阅读教程 \url{https://www.chromium.org/developers/how-tos/trace-event-profiling-tool}。 126 | 127 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black, title=Tip] 128 | llvm-xray工具有更多的功能。可以在LLVM网站上找到它,网址是\url{https://llvm.org/docs/XRay.html}和\url{https://llvm.org/docs/XRayExample.html}。 129 | \end{tcolorbox} 130 | 131 | 本节中,我们学习了如何使用XRay检测应用程序,如何收集运行时信息,以及如何可视化数据。我们可以利用这些知识来发现应用程序中的性能瓶颈。\par 132 | 133 | 识别应用程序中的错误的另一种方法是通过静态分析器分析源代码。\par 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /content/3/chapter11/6.tex: -------------------------------------------------------------------------------- 1 | 静态分析器是一个令人印象深刻的例子,说明了使用Clang基础结构可以做什么。也可以通过插件来扩展Clang,这样你就可以将自己的功能添加到Clang中。该技术与在LLVM中添加Pass插件非常相似。\par 2 | 3 | 用一个简单的插件来探索它的功能。LLVM编码标准要求函数名以小写字母开头。然而,随着时间的推移,编码标准也在不断发展。在许多情况下,需要函数以大写字母开头。一个产生违反命名规则的警告插件可以帮助解决这个问题,所以让我们尝试一下实现这样一个插件。\par 4 | 5 | 因为希望在抽象语法树(AST)上运行用户定义的操作,所以需要定义PluginASTAction类的一个子类。如果使用Clang库来编写自己的工具,可以将动作定义为ASTFrontendAction类的子类。PluginASTAction类是ASTFrontendAction类的一个子类,具有解析命令行选项的额外功能。\par 6 | 7 | 需要的另一个类是ASTConsumer类的子类。AST消费者是一个类,可以使用它在AST上运行操作,而不管AST的来源是什么。我们的第一个插件不需要太多组件,可以在NamingPlugin.cpp文件中创建如下实现:\par 8 | 9 | \begin{enumerate} 10 | \item 首先包括必需的头文件。除了上面提到的ASTConsumer类,还需要一个编译器的实例和插件注册表: 11 | \begin{lstlisting}[caption={}] 12 | #include "clang/AST/ASTConsumer.h" 13 | #include "clang/Frontend/CompilerInstance.h" 14 | #include "clang/Frontend/FrontendPluginRegistry.h" 15 | \end{lstlisting} 16 | 17 | \item 使用clang命名空间,并将实现放到一个匿名命名空间中,以避免名称冲突: 18 | \begin{lstlisting}[caption={}] 19 | using namespace clang; 20 | namespace { 21 | \end{lstlisting} 22 | 23 | \item 接下来,定义ASTConsumer类的子类。稍后,如果检测到违反命名规则,将发出警告。为此,需要一个对DiagnosticsEngine实例的引用。 24 | 25 | \item 需要在类中存储一个CompilerInstance实例,然后可以请求一个DiagnosticsEngine实例: 26 | \begin{lstlisting}[caption={}] 27 | class NamingASTConsumer : public ASTConsumer { 28 | CompilerInstance &CI; 29 | 30 | public: 31 | NamingASTConsumer(CompilerInstance &CI) : CI(CI) {} 32 | \end{lstlisting} 33 | 34 | \item ASTConsumer实例有几个入口方法。HandleTopLevelDecl()方法最符合我们的要求,在顶层的每个声明都调用该方法。这不仅仅包括函数,例如:变量。因此,可以使用LLVM RTTI dyn\underline{~}cast<>()函数来确定该声明是否为函数声明。HandleTopLevelDecl()方法有一个声明组作为参数,它可以包含多个声明。这需要对声明进行循环。下面的代码向我们展示了HandleTop\allowbreak LevelDecl()方法的实现: 35 | \begin{lstlisting}[caption={}] 36 | bool HandleTopLevelDecl(DeclGroupRef DG) override { 37 | for (DeclGroupRef::iterator I = DG.begin(), 38 | E = DG.end(); 39 | I != E; ++I) { 40 | const Decl *D = *I; 41 | if (const FunctionDecl *FD = 42 | dyn_cast(D)) { 43 | \end{lstlisting} 44 | 45 | \item 找到函数声明之后,需要检索函数的名称。还需要确保名称不为空: 46 | \begin{lstlisting}[caption={}] 47 | std::string Name = 48 | FD->getNameInfo().getName().getAsString(); 49 | assert(Name.length() > 0 && 50 | "Unexpected empty identifier"); 51 | \end{lstlisting} 52 | 如果函数名不是以小写字母开头,就违反了命名规则: 53 | \begin{lstlisting}[caption={}] 54 | char &First = Name.at(0); 55 | if (!(First >= 'a' && First <= 'z')) { 56 | \end{lstlisting} 57 | 58 | \item 要发出警告,需要一个DiagnosticsEngine实例。此外,还需要一个消息ID。在Clang内部,消息ID定义为枚举。因为插件不是Clang的一部分,需要创建自定义ID,然后使用它发出警告: 59 | \begin{lstlisting}[caption={}] 60 | DiagnosticsEngine &Diag = 61 | CI.getDiagnostics(); 62 | unsigned ID = Diag.getCustomDiagID( 63 | DiagnosticsEngine::Warning, 64 | "Function name should start with " 65 | "lowercase letter"); 66 | Diag.Report(FD->getLocation(), ID); 67 | \end{lstlisting} 68 | 69 | \item 除了关闭所有的开花括号外,还需要从这个函数返回true,表示处理可以继续: 70 | \begin{lstlisting}[caption={}] 71 | } 72 | } 73 | } 74 | return true; 75 | } 76 | }; 77 | \end{lstlisting} 78 | 79 | \item 接下来,需要创建PluginASTAction子类,实现了Clang调用的接口: 80 | \begin{lstlisting}[caption={}] 81 | class PluginNamingAction : public PluginASTAction { 82 | public: 83 | \end{lstlisting} 84 | 必须实现的第一个方法是CreateASTConsumer()方法,返回NamingASTConsumer类的一个实例。这个方法由Clang调用,传递的CompilerInstance实例可以访问编译器的所有重要类: 85 | \begin{lstlisting}[caption={}] 86 | std::unique_ptr 87 | CreateASTConsumer(CompilerInstance &CI, 88 | StringRef file) override { 89 | return std::make_unique(CI); 90 | } 91 | \end{lstlisting} 92 | 93 | \item 插件还可以访问命令行选项。我们的插件没有命令行参数,只需要返回true来表示成功: 94 | \begin{lstlisting}[caption={}] 95 | bool ParseArgs(const CompilerInstance &CI, 96 | const std::vector &args) 97 | override { 98 | return true; 99 | } 100 | \end{lstlisting} 101 | 102 | \item 插件的操作类型描述了何时调用该操作。默认值是Cmdline,这意味着插件必须在命令行上命名才能调用。需要重写该方法并将其值更改为AddAfterMainAction,插件才能自动运行: 103 | \begin{lstlisting}[caption={}] 104 | PluginASTAction::ActionType getActionType() override { 105 | return AddAfterMainAction; 106 | } 107 | \end{lstlisting} 108 | 109 | \item PluginNamingAction类的实现完成了: 110 | \begin{lstlisting}[caption={}] 111 | }; 112 | } 113 | \end{lstlisting} 114 | 115 | \item 最后,需要注册插件。第一个参数是插件的名称,第二个参数是帮助文本: 116 | \begin{lstlisting}[caption={}] 117 | static FrontendPluginRegistry::Add 118 | X("naming-plugin", "naming plugin"); 119 | \end{lstlisting} 120 | 121 | \end{enumerate} 122 | 123 | 这就完成了插件的实现。要编译插件,在CMakeLists.txt文件中创建一个构建描述。该插件位于Clang源代码树之外,因此需要设置一个完整的项目。可以遵循以下步骤:\par 124 | 125 | \begin{enumerate} 126 | \item 首先定义所需的CMake版本和项目名称: 127 | \begin{tcolorbox}[colback=white,colframe=black] 128 | cmake\underline{~}minimum\underline{~}required(VERSION 3.13.4) \\ 129 | project(naminglugin) 130 | \end{tcolorbox} 131 | 132 | \item 接下来,包括LLVM文件。如果CMake不能自动找到文件,需要设置LLVM\underline{~}DIR变量,以指向包含CMake文件的LLVM目录: 133 | \begin{tcolorbox}[colback=white,colframe=black] 134 | find\underline{~}package(LLVM REQUIRED CONFIG) 135 | \end{tcolorbox} 136 | 137 | \item 将带有CMake文件的LLVM目录添加到搜索路径中,并包含一些必需的模块: 138 | \begin{tcolorbox}[colback=white,colframe=black] 139 | list(APPEND CMAKE\underline{~}MODULE\underline{~}PATH \$\{LLVM\underline{~}DIR\}) \\ 140 | include(ChooseMSVCCRT) \\ 141 | include(AddLLVM) \\ 142 | include(HandleLLVMOptions) 143 | \end{tcolorbox} 144 | 145 | \item 然后,加载Clang的CMake定义。如果CMake不能自动找到文件,那么必须设置Clang\underline{~}DIR变量指向包含CMake文件的Clang目录: 146 | \begin{tcolorbox}[colback=white,colframe=black] 147 | find\underline{~}package(Clang REQUIRED) 148 | \end{tcolorbox} 149 | 150 | \item 接下来,定义头文件和库文件的位置,以及使用哪些定义: 151 | \begin{tcolorbox}[colback=white,colframe=black] 152 | include\underline{~}directories("\$\{LLVM\underline{~}INCLUDE\underline{~}DIR\}" \\ 153 | \hspace*{1cm}"\$\{CLANG\underline{~}INCLUDE\underline{~}DIRS\}") \\ 154 | add\underline{~}definitions("\$\{LLVM\underline{~}DEFINITIONS\}") \\ 155 | link\underline{~}directories("\$\{LLVM\underline{~}LIBRARY\underline{~}DIR\}") 156 | \end{tcolorbox} 157 | 158 | \item 前面的定义设置了构建环境。插入以下命令,定义插件的名称,插件的源文件,以及说明它是一个Clang插件: 159 | \begin{tcolorbox}[colback=white,colframe=black] 160 | add\underline{~}llvm\underline{~}library(NamingPlugin MODULE NamingPlugin.cpp \\ 161 | \hspace*{3cm}PLUGIN\underline{~}TOOL clang) 162 | \end{tcolorbox} 163 | 164 | 在Windows上,插件支持不同于Unix平台,必须链接所需的LLVM和Clang库: 165 | \begin{tcolorbox}[colback=white,colframe=black] 166 | if(LLVM\underline{~}ENABLE\underline{~}PLUGINS AND (WIN32 OR CYGWIN)) \\ 167 | \hspace*{0.5cm}set(LLVM\underline{~}LINK\underline{~}COMPONENTS Support) \\ 168 | \hspace*{0.5cm}clang\underline{~}target\underline{~}link\underline{~}libraries(NamingPlugin PRIVATE \\ 169 | \hspace*{1cm}clangAST clangBasic clangFrontend clangLex) \\ 170 | endif() 171 | \end{tcolorbox} 172 | 173 | \item 将这两个文件保存在NamingPlugin目录中。创建一个与NamingPlugin目录同级的\par build-NamingPlugin目录,并使用以下命令构建插件: 174 | \begin{tcolorbox}[colback=white,colframe=black] 175 | \$ mkdir build-naming-plugin \\ 176 | \$ cd build-naming-plugin \\ 177 | \$ cmake –G Ninja ../NamingPlugin \\ 178 | \$ ninja 179 | \end{tcolorbox} 180 | 181 | \end{enumerate} 182 | 183 | 这些步骤创建NamingPlugin,构建目录中的动态库。\par 184 | 185 | 要测试插件,请将下面的源代码保存为named.c文件。Func1函数名违反了命名规则,但主函数没有违反规则:\par 186 | 187 | \begin{lstlisting}[caption={}] 188 | int Func1() { return 0; } 189 | int main() { return Func1(); } 190 | \end{lstlisting} 191 | 192 | 要调用插件,需要使用-fplugin=指定:\par 193 | 194 | \begin{tcolorbox}[colback=white,colframe=black] 195 | \$ clang -fplugin=./NamingPlugin.so naming.c \\ 196 | naming.c:1:5: warning: Function name should start with \\ 197 | lowercase letter \\ 198 | int Func1() { return 0; } \\ 199 | \hspace*{0.7cm}\^ \\ 200 | 1 warning generated. 201 | \end{tcolorbox} 202 | 203 | 这种调用需要覆盖PluginASTAction类的getActionType()方法,并且返回一个不同于Cmdline默认值的值。\par 204 | 205 | 如果没有这样做,但还想对插件操作的调用有更多的控制,那么可以从编译器命令行运行插件:\par 206 | 207 | \begin{tcolorbox}[colback=white,colframe=black] 208 | \$ clang -cc1 -load ./NamingPlugin.so -plugin naming-plugin$\setminus$ \\ 209 | \hspace*{0.5cm}naming.c 210 | \end{tcolorbox} 211 | 212 | 祝贺你,已经构建了第一个Clang插件!\par 213 | 214 | 这种方法的缺点是它有一定的局限性。ASTConsumer类有不同的入口方法,但它们都是粗粒度的,这可以通过使用RecursiveASTVisitor类来解决。这个类遍历所有AST节点,可以覆盖感兴趣的VisitXXX()方法。可以按照以下步骤来为使用“访问者”重写插件:\par 215 | 216 | \begin{enumerate} 217 | \item 定义RecursiveASTVisitor类需要一个额外的include: 218 | \begin{lstlisting}[caption={}] 219 | #include "clang/AST/RecursiveASTVisitor.h" 220 | \end{lstlisting} 221 | 222 | \item 然后,将访问者定义为匿名名称空间中的第一个类。只存储对AST上下文的引用,将使您访问所有用于AST操作的重要方法,包括发出警告所需的DiagnosticsEngine实例: 223 | \begin{lstlisting}[caption={}] 224 | class NamingVisitor 225 | : public RecursiveASTVisitor { 226 | private: 227 | ASTContext &ASTCtx; 228 | public: 229 | explicit NamingVisitor(CompilerInstance &CI) 230 | : ASTCtx(CI.getASTContext()) {} 231 | \end{lstlisting} 232 | 233 | \item 遍历过程中,只要发现函数声明,就调用VisitFunctionDecl()方法。将内部循环的主体复制到HandleTopLevelDecl()函数中: 234 | \begin{lstlisting}[caption={}] 235 | virtual bool VisitFunctionDecl(FunctionDecl *FD) { 236 | std::string Name = 237 | FD->getNameInfo().getName().getAsString(); 238 | assert(Name.length() > 0 && 239 | "Unexpected empty identifier"); 240 | char &First = Name.at(0); 241 | if (!(First >= 'a' && First <= 'z')) { 242 | DiagnosticsEngine &Diag = 243 | ASTCtx.getDiagnostics(); 244 | unsigned ID = Diag.getCustomDiagID( 245 | DiagnosticsEngine::Warning, 246 | "Function name should start with " 247 | "lowercase letter"); 248 | Diag.Report(FD->getLocation(), ID); 249 | } 250 | return true; 251 | } 252 | }; 253 | \end{lstlisting} 254 | 255 | \item 这就完成了访问者实现。在NamingASTConsumer类中,现在只会存储一个访问者实例: 256 | \begin{lstlisting}[caption={}] 257 | std::unique_ptr Visitor; 258 | public: 259 | NamingASTConsumer(CompilerInstance &CI) 260 | : Visitor(std::make_unique(CI)) {} 261 | \end{lstlisting} 262 | 263 | \item 您将删除HandleTopLevelDecl()方法,因为该功能现在在访问者类中,所以需要重写Handle\allowbreak TranslationUnit()方法。每个翻译单元调用一次,会在这里开始AST遍历: 264 | \begin{lstlisting}[caption={}] 265 | void 266 | HandleTranslationUnit(ASTContext &ASTCtx) override { 267 | Visitor->TraverseDecl( 268 | ASTCtx.getTranslationUnitDecl()); 269 | } 270 | \end{lstlisting} 271 | 272 | \end{enumerate} 273 | 274 | 这个新的实现具有完全相同的功能。优点是它更容易扩展,例如:想检查变量声明,可以实现VisitVarDecl()方法。或者,如果想使用语句,那么可以实现VisitStmt()方法。基本上,对于C、C++和Objective-C语言的每个实体都有一个visitor方法。\par 275 | 276 | 访问AST允许构建执行复杂任务的插件。如本节所述,强制执行命名约定是对Clang的一个补充。另一个可以作为插件实现的有用的附加功能是计算软件度量,比如:圈复杂度。您还可以添加或替换AST节点,例如:允许添加运行时检测。添加插件可以以需要的方式,对Clang进行扩展。\par 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | -------------------------------------------------------------------------------- /content/3/chapter11/7.tex: -------------------------------------------------------------------------------- 1 | 本章中,学习了如何使用各种sanitizer。使用地址sanitizer检测到指针错误,使用内存sanitizer检测到未初始化的内存访问,并使用线程sanitizer检测到数据竞争。应用程序错误通常是由不正确的输入触发的,实现了模糊测试来使用随机数据测试应用程序。\par 2 | 3 | 使用XRay对应用程序进行了测试,以确定性能瓶颈,还了解了可视化数据的各种方法。本章中,还使用了Clang静态分析器来通过解释源代码来查找可能的错误,并学习了如何构建自己的Clang插件。\par 4 | 5 | 这些技能将帮助您提高所构建的应用程序的质量。在应用程序用户抱怨运行时错误之前,找到它们当然是好的。应用本章中获得的知识,不仅可以找到广泛的常见错误,还可以用新的功能扩展Clang。\par 6 | 7 | 下一章中,您将学习如何想LLVM添加一个新的后端。\par 8 | 9 | \newpage -------------------------------------------------------------------------------- /content/3/chapter11/images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/3/chapter11/images/1.jpg -------------------------------------------------------------------------------- /content/3/chapter11/images/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/3/chapter11/images/2.jpg -------------------------------------------------------------------------------- /content/3/chapter11/images/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/3/chapter11/images/3.jpg -------------------------------------------------------------------------------- /content/3/chapter11/images/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/3/chapter11/images/4.jpg -------------------------------------------------------------------------------- /content/3/chapter12/0.tex: -------------------------------------------------------------------------------- 1 | LLVM有一个非常灵活的架构。您可以向它添加一个新的后端,后端的核心是目标描述,大部分代码都是从它生成的。但是,还不可能生成一个完整的后端,并且实现调用规则需要进行手动编码。本章中,我们将学习如何添加对老CPU的支持。\par 2 | 3 | 本章中,我们将学习以下内容:\par 4 | 5 | \begin{itemize} 6 | \item 设置一个新的后端时,我们将向您介绍M88k CPU体系架构,并向您展示在哪里可以找到您需要的信息。 7 | 8 | \item 将新的体系结构添加到Triple类中,将教会您如何让LLVM了解新的CPU体系架构。 9 | 10 | \item 在扩展LLVM中的ELF文件格式定义时,可以向处理ELD对象文件的库和工具中添加对m88k特定重定位的支持。 11 | 12 | \item 创建目标描述时,您将使用TableGen语言开发目标描述的所有部分。 13 | 14 | \item 实现DAG指令选择类时,您将创建指令选择所需的Pass和支持的类。 15 | 16 | \item 生成汇编指令时,会带您了解如何实现汇编打印,生成汇编文本。 17 | 18 | \item 生成的机器码时,您将了解必须提供哪些附加类才能使机器码(MC)层将代码写入目标文件。 19 | 20 | \item 添加反汇编支持时,您将了解如何实现对反汇编程序的支持。 21 | 22 | \item 将这些功能整合在一起时,就可以将新后端的源代码集成到构建系统中。 23 | \end{itemize} 24 | 25 | 本章结束时,您将了解如何开发一个新的和完整的后端。您将了解后台的不同组成部分,从而更深入地了解LLVM体系结构。\par 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /content/3/chapter12/1.tex: -------------------------------------------------------------------------------- 1 | 本章的代码文件可在\url{https://github.com/PacktPublishing/Learn-LLVM-12/tree/master/Chapter12}获取。\par 2 | 3 | 你可以在视频中找到代码\url{https://bit.ly/3nllhED}。\par -------------------------------------------------------------------------------- /content/3/chapter12/10.tex: -------------------------------------------------------------------------------- 1 | 我们的新目标位于llvm/lib/Target/M88k目录中,需要集成到构建系统中。为了简化开发,我们将其作为实验目标添加到llvm/CMakeLists.txt文件中。将现有的空字符串替换为目标的名称:\par 2 | 3 | \begin{tcolorbox}[colback=white,colframe=black] 4 | set(LLVM\underline{~}EXPERIMENTAL\underline{~}TARGETS\underline{~}TO\underline{~}BUILD "M88k" … ) 5 | \end{tcolorbox} 6 | 7 | 我们还需要提供llvm/lib/Target/M88k/CMakeLists.txt来构建目标。除了列出目标的C++文件外,还定义了从目标描述生成源文件的方法。\par 8 | 9 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black, title=从目标描述生成所有类型的源码] 10 | llvm-tblgen工具的不同运行方式,会生成C++代码的不同部分。但是,建议将所有部件的生成添加到CMakeLists.txt文件中。这样做的原因是能提供了更好的检查,例如:如果您在指令编码方面犯了错误,那么这只会在反汇编程序生成代码期间捕获。因此,即使不打算支持反汇编程序,仍值得为其生成源码。 11 | \end{tcolorbox} 12 | 13 | 该文件如下所示:\par 14 | 15 | \begin{enumerate} 16 | \item 首先,定义一个名为M88k的新LLVM组件: 17 | \begin{tcolorbox}[colback=white,colframe=black] 18 | add\underline{~}llvm\underline{~}component\underline{~}group(M88k) 19 | \end{tcolorbox} 20 | 21 | \item 接下来,命名目标描述文件,添加语句以使用TableGen生成各种源片段,并为其定义一个公共目标: 22 | \begin{tcolorbox}[colback=white,colframe=black] 23 | set(LLVM\underline{~}TARGET\underline{~}DEFINITIONS M88k.tdtablegen(LLVM \\ 24 | M88kGenAsmMatcher.inc -gen-asm-matcher) \\ 25 | tablegen(LLVM M88kGenAsmWriter.inc -gen-asm-writer) \\ 26 | tablegen(LLVM M88kGenCallingConv.inc -gen-callingconv) \\ 27 | tablegen(LLVM M88kGenDAGISel.inc -gen-dag-isel) \\ 28 | tablegen(LLVM M88kGenDisassemblerTables.inc \\ 29 | \hspace*{8cm}-gen-disassembler) \\ 30 | tablegen(LLVM M88kGenInstrInfo.inc -gen-instr-info) \\ 31 | tablegen(LLVM M88kGenMCCodeEmitter.inc -gen-emitter) \\ 32 | tablegen(LLVM M88kGenRegisterInfo.inc -gen-register-info) \\ 33 | tablegen(LLVM M88kGenSubtargetInfo.inc -gen-subtarget) \\ 34 | add\underline{~}public\underline{~}tablegen\underline{~}target(M88kCommonTableGen) 35 | \end{tcolorbox} 36 | 37 | \item 必须列出新组件的所有源文件: 38 | \begin{tcolorbox}[colback=white,colframe=black] 39 | add\underline{~}llvm\underline{~}target(M88kCodeGen \\ 40 | \hspace*{0.5cm}M88kAsmPrinter.cpp M88kFrameLowering.cpp \\ 41 | \hspace*{0.5cm}M88kISelDAGToDAG.cpp M88kISelLowering.cpp \\ 42 | \hspace*{0.5cm}M88kRegisterInfo.cpp M88kSubtarget.cpp \\ 43 | \hspace*{0.5cm}M88kTargetMachine.cpp ) 44 | \end{tcolorbox} 45 | 46 | \item 最后,在构建中包含包含MC和反汇编类的目录: 47 | \begin{tcolorbox}[colback=white,colframe=black] 48 | add\underline{~}subdirectory(MCTargetDesc) \\ 49 | add\underline{~}subdirectory(Disassembler) 50 | \end{tcolorbox} 51 | 52 | \end{enumerate} 53 | 54 | 现在我们准备用新的后端目标编译LLVM。在构建目录下,可以运行如下代码:\par 55 | 56 | \begin{tcolorbox}[colback=white,colframe=black] 57 | \$ ninja 58 | \end{tcolorbox} 59 | 60 | 这将检测更改的CMakeLists.txt文件,再次运行配置步骤,并编译新的后端。要检查是否一切正常,可以运行以下命令:\par 61 | 62 | \begin{tcolorbox}[colback=white,colframe=black] 63 | \$ bin/llc –version 64 | \end{tcolorbox} 65 | 66 | 注册目标的输出:\par 67 | 68 | \begin{tcolorbox}[colback=white,colframe=black] 69 | m88k \hspace{2cm} - M88k 70 | \end{tcolorbox} 71 | 72 | 噢耶!我们完成了后端实现。下面LLVM IR中的f1函数在函数的两个参数之间执行一个按位的AND操作并返回结果。将其保存成example.ll文件:\par 73 | 74 | \begin{tcolorbox}[colback=white,colframe=black] 75 | target triple = "m88k-openbsd" \\ 76 | define i32 @f1(i32 \%a, i32 \%b) \{ \\ 77 | \hspace*{0.5cm}\%res = and i32 \%a, \%b \\ 78 | \hspace*{0.5cm}ret i32 \%res \\ 79 | \} 80 | \end{tcolorbox} 81 | 82 | 运行llc工具,在控制台上查看生成的汇编文本:\par 83 | 84 | \begin{tcolorbox}[colback=white,colframe=black] 85 | \$ llc < example.ll \\ 86 | \hspace*{1cm}.text \\ 87 | \hspace*{1cm}.file \hspace{0.8cm}"" \\ 88 | \hspace*{1cm}.globl \hspace{0.5cm}f1 \hspace{6cm} \# \verb|--| Begin \\ 89 | function f1 \\ 90 | \hspace*{1cm}.align \hspace{0.5cm} 3 \\ 91 | \hspace*{1cm}.type\hspace{0.8cm}f1,@function \\ 92 | f1: \hspace{8cm} \# @f1 \\ 93 | \hspace*{1cm}.cfi\underline{~}startproc \\ 94 | \# \%bb.0: \\ 95 | \hspace*{1cm}and \%r2, \%r2, \%r3 \\ 96 | \hspace*{1cm}jmp \%r1 \\ 97 | .Lfunc\underline{~}end0: \\ 98 | \hspace*{1cm}.size\hspace{1cm}f1, .Lfunc\underline{~}end0-f1 \\ 99 | \hspace*{1cm}.cfi\underline{~}endproc \\ 100 | \hspace*{8.8cm}\# -- End function \\ 101 | \hspace*{1cm}.section\hspace{1cm}".note.GNU-stack","",@progbits 102 | \end{tcolorbox} 103 | 104 | 输出是有效的GNU语法。对于f1函数,将生成和和jmp指令。参数在\%r2和\%r3寄存器中传递,这两个寄存器在and指令中使用。结果存储在\%r2寄存器中,该寄存器也是返回32位值的寄存器。函数的返回通过分支到\%r1寄存器中的hold地址实现,该地址也与ABI匹配。一切看起来都很好!\par 105 | 106 | 学习了本章中的内容,现在可以实现自己的LLVM后端。对于许多相对简单的CPU,如:数字信号处理器(DSP),只需要实现这些功能就够用了。当然,M88k CPU体系结构的实现还不支持该体系架构的所有特性,例如:浮点寄存器。不过,现在已经了解了LLVM后端开发中所有重要概念,有了这些知识,您将能够添加任何缺失的部分!\par 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /content/3/chapter12/11.tex: -------------------------------------------------------------------------------- 1 | 本章中,学习了如何为LLVM开发一个新的后端目标。首先收集了所需的文档,并通过增强Triple类使LLVM了解了新的体系结构。该文档还包括ELF文件格式的重定位定义,并向LLVM添加了对此的支持。\par 2 | 3 | 了解了目标描述包含的不同部分,并使用它生成C++代码,了解了如何实现指令选择。为了输出生成的代码,开发了一个汇编打印器,并了解了需要哪些支持类来写入目标文件。还学习了如何添加对反汇编的支持,反汇编用于将目标文件转换回汇编文本。最后,扩展了构建系统以在构建中包含新目标。\par 4 | 5 | 现在,您已经具备了在自己的项目中创造性地使用LLVM所需的一切。LLVM生态系统是非常活跃的,一直在添加新功能,所以请务必跟上它的发展!\par 6 | 7 | 作为一名编译器开发人员,我很高兴能够写关于LLVM的文章,并在此过程中发现了一些新的特性。希望你也能在LLVM中玩得开心! :) 8 | 9 | \newpage -------------------------------------------------------------------------------- /content/3/chapter12/2.tex: -------------------------------------------------------------------------------- 1 | 无论是商业上需要支持一个新的CPU,还是只是一个业余项目需要添加对一些旧架构的支持,为LLVM添加一个新后端都是一项重要任务。以下部分概述了开发新后端所需的内容。我们将为摩托罗拉M88k架构添加一个后端,这是一个20世纪80年代的RISC架构。\par 2 | 3 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black, title=References] 4 | \hspace*{0.7cm}可以在维基百科上阅读更多关于该架构的信息:\url{https://en.wikipedia.org/wiki/Motorola_88000}。关于这个体系结构的重要信息仍然可以在互联网上找到,可以在\url{http://www.bitsavers.org/components/motorola/88000/}找到带有指令集和计时信息的CPU手册,和SystemV ABI M88k处理器补充ELF格式的定义和调用规则可以在\url{https://archive.org/details/bitsavers_attunixSysa0138776555SystemVRelease488000ABI1990_8011463}找到。\par 5 | 6 | \hspace*{0.7cm}OpenBSD(可在\url{https://www.openbsd.org/}获得)仍然支持LUNA-88k系统。在OpenBSD系统上,很容易为M88k创建一个GCC交叉编译器。GXemul可在\url{http://gavare。se/gxemul/}查看相应资料,并且其有一个模拟器能够运行针对M88k体系结构的某些OpenBSD版本。 7 | \end{tcolorbox} 8 | 9 | 总的来说,M88k体系结构已经淘汰很久了,但是我们找到了足够的信息和工具,可以为它添加一个LLVM后端。我们将从一个非常基本的任务开始,并扩展到Triple类。\par 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /content/3/chapter12/3.tex: -------------------------------------------------------------------------------- 1 | Triple类的一个实例表示LLVM为其生成代码的目标平台。为了支持新的体系架构,第一个任务是扩展Triple类。在llvm/include/llvm/ADT/Triple.h文件中,可向ArchType枚举添加了一个成员和一个新的谓词:\par 2 | 3 | \begin{lstlisting}[caption={}] 4 | class Triple { 5 | public: 6 | enum ArchType { 7 | // Many more members 8 | m88k, // M88000 (big endian): m88k 9 | }; 10 | /// Tests whether the target is M88k. 11 | bool isM88k() const { 12 | return getArch() == Triple::m88k; 13 | } 14 | // Many more methods 15 | }; 16 | \end{lstlisting} 17 | 18 | 在llvm/lib/Support/Triple.cpp文件中,有许多使用ArchType枚举的地方。需要扩展它们,例如:在getArchTypeName()方法中,添加一个新的case:\par 19 | 20 | \begin{lstlisting}[caption={}] 21 | switch (Kind) { 22 | // Many more cases 23 | case m88k: return "m88k"; 24 | } 25 | \end{lstlisting} 26 | 27 | 大多数情况下,如果忘记在某个函数中处理新的m88k枚举成员,编译器会发出警告。接下来,我们将扩展可执行和可链接格式(ELF)定义。\par 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /content/3/chapter12/4.tex: -------------------------------------------------------------------------------- 1 | ELF文件格式是LLVM支持读写的二进制对象文件格式之一。ELF本身是为许多CPU架构结构定义的,对于M88k体系架构也有一个定义。我们需要做的就是添加重定位的定义和一些标志。重定位可在System V ABI M88k处理器手册的第4章看到:\par 2 | 3 | \begin{enumerate} 4 | \item 需要在llvm/include/llvm/BinaryFormat/ELFRelocs/M88k.def文件中输入以下内容: 5 | \begin{lstlisting}[caption={}] 6 | #ifndef ELF_RELOC 7 | #error "ELF_RELOC must be defined" 8 | #endif 9 | ELF_RELOC(R_88K_NONE, 0) 10 | ELF_RELOC(R_88K_COPY, 1) 11 | // Many more… 12 | \end{lstlisting} 13 | 14 | \item 还需要在llvm/include/llvm/BinaryFormat/ELF.h文件中添加了一些标志,并包含了重定位定义: 15 | \begin{lstlisting}[caption={}] 16 | // M88k Specific e_flags 17 | enum : unsigned { 18 | EF_88K_NABI = 0x80000000, // Not ABI compliant 19 | EF_88K_M88110 = 0x00000004 // File uses 88110- 20 | // specific 21 | // features 22 | }; 23 | // M88k relocations. 24 | enum { 25 | #include "ELFRelocs/M88k.def" 26 | }; 27 | \end{lstlisting} 28 | 代码可以添加到文件中的任何地方,但最好保持排序顺序,并在MIPS体系结构的代码之前插入它。 29 | 30 | \item 还需要扩展其他一些方法。在llvm/include/llvm/Object/ELFObjectFile.h文件中,有一些方法可以在枚举成员和字符串之间进行转换,例如:必须在getFileFormatName()方法中添加新的case语句: 31 | \begin{lstlisting}[caption={}] 32 | switch (EF.getHeader()->e_ident[ELF::EI_CLASS]) { 33 | // Many more cases 34 | case ELF::EM_88K: 35 | return "elf32-m88k"; 36 | } 37 | \end{lstlisting} 38 | 39 | \item 类似地,扩展了getArch()方法。 40 | 41 | \item 最后,在llvm/lib/Object/ELF.cpp文件的getELFRelocationTypeName()方法中使用重定位定义: 42 | \begin{lstlisting}[caption={}] 43 | switch (Machine) { 44 | // Many more cases 45 | case ELF::EM_88K: 46 | switch (Type) { 47 | #include "llvm/BinaryFormat/ELFRelocs/M88k.def" 48 | default: 49 | break; 50 | } 51 | break; 52 | } 53 | \end{lstlisting} 54 | 55 | \item 为了完成支持,还可以在llvm/lib/ObjectYAML/ELFYAML.cpp文件中,映射ELFYAML::\allowbreak ELF\underline{~}REL枚举的方法中添加重定位。 56 | 57 | \item 至此,我们已经以ELF文件格式完成了对m88k体系结构的支持。可以使用llvm-readobj工具检查ELF对象文件,例如:由OpenBSD上的交叉编译器创建的文件。同样,可以使用yaml2obj工具为m88k体系结构创建一个ELF对象文件。 58 | 59 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black, title=是否必须添加对对象文件格式的支持?] 60 | 61 | 将架构支持集成到ELF文件格式实现中只需要几行代码。如果为其创建LLVM后端的体系结构使用ELF格式,那么应该采用此方法。另一方面,添加对全新二进制文件格式的支持本身就是一项复杂的任务。在这种情况下,一种可能的方法是只输出汇编程序文件,并使用外部汇编程序创建对象文件。 62 | \end{tcolorbox} 63 | 64 | \end{enumerate} 65 | 66 | 通过这些添加,ELF文件格式的实现现在支持M88k体系结构。下一节中,我们将创建M88k体系结构的描述,该描述介绍了该体系结构的指令、寄存器、调用规则,以及其他细节。\par 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /content/3/chapter12/5.tex: -------------------------------------------------------------------------------- 1 | 2 | 目标描述是后端实现的核心。理想情况下,可以从目标描述生成整个后端。这个目标还没有达到,因此,需要在以后扩展生成的代码。让我们从顶层文件开始剖析目标。\par 3 | 4 | \hspace*{\fill} \par %插入空行 5 | \textbf{实现目标描述的顶层文件} 6 | 7 | 我们将新后端文件放入llvm/lib/Target/M88k目录,目标描述在M88k.td文件中:\par 8 | 9 | \begin{enumerate} 10 | \item 这个文件中,首先需要包含由LLVM预定义的基本目标描述类,然后是在下一节中要创建的文件: 11 | \begin{tcolorbox}[colback=white,colframe=black] 12 | include "llvm/Target/Target.td" \\ 13 | \\ 14 | include "M88kRegisterInfo.td" \\ 15 | include "M88kCallingConv.td" \\ 16 | include "M88kSchedule.td" \\ 17 | include "M88kInstrFormats.td" \\ 18 | include "M88kInstrInfo.td" 19 | \end{tcolorbox} 20 | 21 | \item 接下来,还要定义受支持的处理器。除此之外,也可以转换为-mcpu=option的参数: 22 | \begin{tcolorbox}[colback=white,colframe=black] 23 | def : ProcessorModel<"mc88110", M88kSchedModel, []>; 24 | \end{tcolorbox} 25 | 26 | \item 完成所有这些定义后,就可以将目标拼凑在一起了。定义了这些子类,为了可以修改默认值。M88kInstrInfo类保存了关于指令的所有信息: 27 | \begin{tcolorbox}[colback=white,colframe=black] 28 | def M88kInstrInfo : InstrInfo; 29 | \end{tcolorbox} 30 | 31 | \item 为.s汇编文件定义了一个解析器,声明寄存器名称时总是以\%作为前缀: 32 | \begin{tcolorbox}[colback=white,colframe=black] 33 | def M88kAsmParser : AsmParser; \\ 34 | def M88kAsmParserVariant : AsmParserVariant \{ \\ 35 | \hspace*{1cm}let RegisterPrefix = "\%"; \\ 36 | \} 37 | \end{tcolorbox} 38 | 39 | \item 接下来,为程序集编写器定义一个类,负责编写.s汇编文件: 40 | \begin{tcolorbox}[colback=white,colframe=black] 41 | def M88kAsmWriter : AsmWriter; 42 | \end{tcolorbox} 43 | 44 | \item 最后,将所有这些记录放在一起来定义目标: 45 | \begin{tcolorbox}[colback=white,colframe=black] 46 | def M88k : Target \{ \\ 47 | \hspace*{1cm}let InstructionSet = M88kInstrInfo; \\ 48 | \hspace*{1cm}let AssemblyParsers = [M88kAsmParser]; \\ 49 | \hspace*{1cm}let AssemblyParserVariants = [M88kAsmParserVariant]; \\ 50 | \hspace*{1cm}let AssemblyWriters = [M88kAsmWriter]; \\ 51 | \hspace*{1cm}let AllowRegisterRenaming = 1; \\ 52 | \} 53 | \end{tcolorbox} 54 | 55 | \end{enumerate} 56 | 57 | 现在已经实现了顶层文件,我们将创建包含的文件,从寄存器定义开始。\par 58 | 59 | \hspace*{\fill} \par %插入空行 60 | \textbf{添加寄存器定义} 61 | 62 | CPU体系架构通常定义一组寄存器,这些寄存器的特性可以有很大的不同。一些架构允许访问子寄存器,例如:x86体系结构有特殊的寄存器名,只能访问寄存器值的一部分,其他体系结构没有实现这一点。除了通用寄存器、浮点寄存器和向量寄存器外,体系结构还可以定义特殊寄存器,例如:状态码寄存器或浮点操作配置寄存器。您需要为LLVM定义这些信息。\par 63 | 64 | M88k体系结构定义了通用寄存器、浮点寄存器和控制寄存器。为了使示例简短,我们将只定义通用寄存器。我们从为寄存器定义一个超类开始,寄存器的编码只使用5位,这也限制了保存编码的字段。我们还定义所有生成的C++代码应该放在M88k命名空间中:\par 65 | 66 | \begin{tcolorbox}[colback=white,colframe=black] 67 | class M88kReg Enc, string n> : Register \{ \\ 68 | \hspace*{1cm}let HWEncoding{15-5} = 0; \\ 69 | \hspace*{1cm}let HWEncoding{4-0} = Enc; \\ 70 | \hspace*{1cm}let Namespace = "M88k"; \\ 71 | \} 72 | \end{tcolorbox} 73 | 74 | M88kReg类用于所有寄存器类型,为通用寄存器定义了一个特殊的类:\par 75 | 76 | \begin{tcolorbox}[colback=white,colframe=black] 77 | class GRi Enc, string n> : M88kReg; 78 | \end{tcolorbox} 79 | 80 | 现在可以定义所有通用寄存器(32个)了:\par 81 | 82 | \begin{tcolorbox}[colback=white,colframe=black] 83 | foreach I = 0-31 in \{ \\ 84 | \hspace*{1cm}def R\#I : GRi; \\ 85 | \} 86 | \end{tcolorbox} 87 | 88 | 单个寄存器需要分组到寄存器类中,寄存器的顺序还定义了寄存器分配器中的分配顺序。这里,可以简单地添加所有寄存器:\par 89 | 90 | \begin{tcolorbox}[colback=white,colframe=black] 91 | def GPR : RegisterClass<"M88k", [i32], 32, \\ 92 | \hspace*{4.5cm}(add (sequence "R\%u", 0, 31))>; 93 | \end{tcolorbox} 94 | 95 | 最后,需要定义一个基于寄存器类的操作数。该操作数用于选择与寄存器匹配的DAG节点,它还可以扩展为在汇编代码中打印和匹配寄存器的方法名:\par 96 | 97 | \begin{tcolorbox}[colback=white,colframe=black] 98 | def GPROpnd : RegisterOperand; 99 | \end{tcolorbox} 100 | 101 | 这就完成了寄存器的定义。下一节中,将使用这些定义来定义调用规则。\par 102 | 103 | \hspace*{\fill} \par %插入空行 104 | \textbf{定义调用规则} 105 | 106 | 调用规则定义了如何将参数传递给函数。通常,第一个参数在寄存器中传递,其余的参数在堆栈上传递。还必须有关于如何传递聚合和如何从函数返回值的规则。根据这里给出的定义,将生成分析程序类,稍后在底层调用时使用它们。\par 107 | 108 | 您可以在System V ABI M88k Processor手册的第3章,“底层系统信息”中阅读有关M88k架构上使用的调用规则。这里,我们把它翻译成TableGen语法:\par 109 | 110 | \begin{enumerate} 111 | \item 为调用规则定义了一个记录: 112 | \begin{tcolorbox}[colback=white,colframe=black] 113 | def CC\underline{~}M88k : CallingConv<[ 114 | \end{tcolorbox} 115 | 116 | \item M88k架构只有32位寄存器,因此较小的数据类型的值需要提升到32位: 117 | \begin{tcolorbox}[colback=white,colframe=black] 118 | \hspace*{0.5cm}CCIfType<[i1, i8, i16], CCPromoteToType>, 119 | \end{tcolorbox} 120 | 121 | \item 调用规则规定,对于聚合返回值,指向内存的指针会传递到r12寄存器中: 122 | \begin{tcolorbox}[colback=white,colframe=black] 123 | \hspace*{0.5cm}CCIfSRet>>, 124 | \end{tcolorbox} 125 | 126 | \item 寄存器r2到r9用于传递参数: 127 | \begin{tcolorbox}[colback=white,colframe=black] 128 | \hspace*{0.5cm}CCIfType<[i32,i64,f32,f64], \\ 129 | \hspace*{1cm}CCAssignToReg<[R2, R3, R4, R5, R6, R7, R8, \\ 130 | \hspace*{1.5cm}R9]>>, 131 | \end{tcolorbox} 132 | 133 | \item 每个附加的参数在堆栈上进行传递,需要4字节对齐: 134 | \begin{tcolorbox}[colback=white,colframe=black] 135 | \hspace*{0.5cm}CCAssignToStack<4, 4>, \\ 136 | >; 137 | \end{tcolorbox} 138 | 139 | \item 另一条记录定义如何将结果传递给调用函数。32位值在r2寄存器中传递,64位值使用r2和r3寄存器: 140 | \begin{tcolorbox}[colback=white,colframe=black] 141 | def RetCC\underline{~}M88k : CallingConv<[ \\ 142 | \hspace*{0.5cm}CCIfType<[i32,f32], CCAssignToReg<[R2]>>, \\ 143 | \hspace*{0.5cm}CCIfType<[i64,f64], CCAssignToReg<[R2, R3]>> \\ 144 | ]>; 145 | \end{tcolorbox} 146 | 147 | \item 最后,调用规则还规定了调用函数必须保留哪些寄存器: 148 | \begin{tcolorbox}[colback=white,colframe=black] 149 | def CSR\underline{~}M88k : \\ 150 | \hspace*{1.5cm}CalleeSavedRegs<(add (sequence "R\%d", 14, \\ 151 | \hspace*{2cm}25), R30)>; 152 | \end{tcolorbox} 153 | 154 | \end{enumerate} 155 | 156 | 如果需要,还可以定义多个调用规则。下一节中,我们将简要介绍调度模型。\par 157 | 158 | 159 | \hspace*{\fill} \par %插入空行 160 | \textbf{创建调度模型} 161 | 162 | 使用调度模型代码生成,可以以最优方式对指令进行排序。定义一个调度模型可以提高所生成代码的性能,但不是代码生成所必需的。因此,我们只为模型定义一个占位符。我们添加的CPU一次最多可以发出两条指令的信息,并且它是有序CPU:\par 163 | 164 | \begin{tcolorbox}[colback=white,colframe=black] 165 | def M88kSchedModel : SchedMachineModel \{ \\ 166 | \hspace*{1cm}let IssueWidth = 2; \\ 167 | \hspace*{1cm}let MicroOpBufferSize = 0; \\ 168 | \hspace*{1cm}let CompleteModel = 0; \\ 169 | \hspace*{1cm}let NoModel = 1; \\ 170 | \} 171 | \end{tcolorbox} 172 | 173 | 可以在YouTube上的“编写伟大的调度程序”(Writing Great Schedulers)讲座中找到关于如何创建完整调度模型的方法,网址是\url{https://www.youtube.com/watch?v=brpomKUynEA}。\par 174 | 175 | 接下来,来定义指令格式和指令。\par 176 | 177 | \hspace*{\fill} \par %插入空行 178 | \textbf{定义指令格式和指令信息} 179 | 180 | 我们已经在第9章,支持新机器指令的章节中了解了指令格式和指令信息。为了定义M88k体系结构的指令,我们采用相同的方法。首先,为指令记录定义一个基类。这个类最重要的字段是Inst字段,保存指令的编码。大多数其他字段定义只是给指令超类中定义的字段赋值:\par 181 | 182 | \begin{tcolorbox}[colback=white,colframe=black] 183 | class InstM88k pattern, InstrItinClass itin = \\ 185 | \hspace*{2.5cm}NoItinerary> \\ 186 | \hspace*{1.3cm}: Instruction \{ \\ 187 | \hspace*{1cm}field bits<32> Inst; \\ 188 | \hspace*{1cm}field bits<32> SoftFail = 0; \\ 189 | \hspace*{1cm}let Namespace = "M88k"; \\ 190 | \hspace*{1cm}let Size = 4; \\ 191 | \hspace*{1cm}dag OutOperandList = outs; \\ 192 | \hspace*{1cm}dag InOperandList = ins; \\ 193 | \hspace*{1cm}let AsmString = asmstr; \\ 194 | \hspace*{1cm}let Pattern = pattern; \\ 195 | \hspace*{1cm}let DecoderNamespace = "M88k"; \\ 196 | \hspace*{1cm}let Itinerary = itin; \\ 197 | \} 198 | \end{tcolorbox} 199 | 200 | 这个基类用于所有的指令格式,所以也用于F\underline{~}JMP格式。您需要根据处理器用户手册的介绍,对处理器进行编码。类有两个参数(是编码的一部分),func参数定义了编码的第11位到第15位,它将指令定义为带有或不保存返回地址的跳转。下一个参数是一个位,它定义下一条指令是否无条件执行。这类似于MIPS架构的延迟槽。\par 201 | 202 | 该类还定义了rs2字段,该字段保存保存目标地址的寄存器的编码。其他参数包括DAG输入和输出操作数、文本汇编字符串、用于选择该指令的DAG模式,以及用于调度程序模型的itinerary类:\par 203 | 204 | \begin{tcolorbox}[colback=white,colframe=black] 205 | class F\underline{~}JMP func, bits<1> next, \\ 206 | \hspace*{3cm}dag outs, dag ins, string asmstr, \\ 207 | \hspace*{3cm}list pattern, \\ 208 | \hspace*{3cm}InstrItinClass itin = NoItinerary> \\ 209 | \hspace*{1.3cm}: InstM88k \{ \\ 210 | \hspace*{1cm}bits<5> rs2; \\ 211 | \hspace*{1cm}let Inst{31-26} = 0b111101; \\ 212 | \hspace*{1cm}let Inst{25-16} = 0b0000000000; \\ 213 | \hspace*{1cm}let Inst{15-11} = func; \\ 214 | \hspace*{1cm}let Inst{10} = next; \\ 215 | \hspace*{1cm}let Inst{9-5} = 0b00000; \\ 216 | \hspace*{1cm}let Inst{4-0} = rs2; \\ 217 | \} 218 | \end{tcolorbox} 219 | 220 | 有了这个,就可以定义指令了。跳转指令是基本块中的最后一条指令,因此需要设置isTerminator标志。因为控制流不能通过这条指令,所以还必须设置isBarrier标志。我们从处理器的用户手册中获取func和next参数的值。\par 221 | 222 | 输入DAG操作数是一个通用寄存器,它引用前面寄存器的信息中的操作数。编码存储在rs2字段中,来自前面的类定义,输出操作数为空。汇编字符串给出指令的文本语法,也引用寄存器操作数。DAG模式使用预定义的brind操作符,如果DAG包含一个间接分支节点,目标地址保存在寄存器中,则选择此指令:\par 223 | 224 | \begin{tcolorbox}[colback=white,colframe=black] 225 | let isTerminator = 1, isBarrier = 1 in \\ 226 | \hspace*{1cm}def JMP : F\underline{~}JMP<0b11000, 0, (outs), (ins GPROpnd:\$rs2), \\ 227 | \hspace*{4cm}"jmp \$rs2", [(brind GPROpnd:\$rs2)]>; 228 | \end{tcolorbox} 229 | 230 | 我们需要以这种方式为所有指令进行定义。\par 231 | 232 | 这个文件中,还实现了用于指令选择的其他模式。一个典型的应用是不断的合成,M88k体系架构是32位的,但可以将作为操作数的指令也能支持16位范围的常量。因此,诸如按位运算以及寄存器和32位常量之间的运算,必须分割成两个使用16位常量的指令。\par 233 | 234 | 幸运的是,and指令中的一个标志定义了一个操作是用于寄存器的下半部分还是上半部分。使用操作符LO16和HI16来提取一个常数的下半部分或上半部分,我们可以为一个寄存器和一个32位宽常数之间的和运算建立一个DAG模式:\par 235 | 236 | \begin{tcolorbox}[colback=white,colframe=black] 237 | def : Pat<(and GPR:\$rs1, uimm32:\$imm), \\ 238 | \hspace*{2cm}(ANDri (ANDriu GPR:\$rs1, (HI16 i32:\$imm)), \\ 239 | \hspace*{6cm}(LO16 i32:\$imm))>; 240 | \end{tcolorbox} 241 | 242 | ANDri运算符是将常数应用到寄存器下半部分的and指令,ANDriu运算符使用寄存器上半部分。当然,在使用这些模式之前,必须像定义jmp指令一样定义指令。该模式使用带有and操作的32位常量来解决问题,在指令选择期间为其生成两条机器指令。\par 243 | 244 | 不是所有的操作都可以用预定义的DAG节点来表示,例如:M88k体系结构定义了位域操作,可以将其视为正常和/或操作的一般化。对于这样的操作,可以引入新的节点类型,例如:set指令:\par 245 | 246 | \begin{tcolorbox}[colback=white,colframe=black] 247 | def m88k\underline{~}set : SDNode<"M88kISD::SET", SDTIntBinOp>; 248 | \end{tcolorbox} 249 | 250 | 这定义了一个SDNode类的新记录。第一个参数是表示新操作的C++枚举成员。第二个参数是所谓的类型概要文件,它定义了参数的类型、数量和结果类型。预定义的SDTIntBinOp类定义了两个整型参数和一个整型结果类型。可以在llvm/include/llvm/Target/TargetSelectionDAG.td文件中查找预定义的类。如果没有合适的预定义类型说明文件,那么可以定义一个新的。\par 251 | 252 | 对于调用函数,LLVM需要某些无法预定义的定义,因为它们不是完全独立于目标的,例如:对于returns,我们需要指定一个retflag记录:\par 253 | 254 | \begin{tcolorbox}[colback=white,colframe=black] 255 | def retflag : SDNode<"M88kISD::RET\underline{~}FLAG", SDTNone, \\ 256 | \hspace*{3cm}[SDNPHasChain, SDNPOptInGlue, SDNPVariadic]>; 257 | \end{tcolorbox} 258 | 259 | 与m88k\underline{~}set相比,这还为DAG节点定义了一些标志:使用了链和胶水序列,操作符可以接受可变数量的参数。\par 260 | 261 | \begin{tcolorbox}[colback=blue!5!white,colframe=blue!75!black, title=以迭代的方式实现指令] 262 | 263 | 现代的CPU可以很容易地拥有数千条指令,不一次性实现所有指令是有意义的。相反,您应该首先关注基本指令,如逻辑操作和调用和返回指令。这足以使一个非常基本的后端工作,然后在这个基础上添加越来越多的指令定义和模式。 264 | \end{tcolorbox} 265 | 266 | 这就完成了目标描述的实现。从目标描述中,使用llvm-tblgen工具自动生成大量代码。为了完成指令选择和后台的其他部分,我们仍需要使用生成的代码开发C++代码。下一节中,我们将实现DAG指令的选择。\par 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /content/3/chapter12/7.tex: -------------------------------------------------------------------------------- 1 | 前几节中实现的指令选择将IR指令降到MachineInstr实例中。这是一个非常底层的指令,但还不是机器代码。后端Pass中的最后一个步骤是发出指令,要么作为汇编文本,要么作为目标文件。M88kAsmPrinter机器Pass负责这项任务。\par 2 | 3 | 基本上,这个Pass将一个MachineInstr实例降到一个MCInst实例,然后该实例发到一个streamer中。因为MachineInstr类没有所需的细节,所以MCInst类表示实际机器代码指令。\par 4 | 5 | 对于第一种方法,我们可以将实现限制为emitInstruction()方法。需要重写更多方法来支持几种操作数类型,主要是为了发出正确的重定位。这个类还负责处理内联汇编程序,如果需要,也要实现内联汇编程序。\par 6 | 7 | 因为M88kAsmPrinter类是机器函数Pass,所以我们还重写了getPassName()方法。类的声明如下:\par 8 | 9 | \begin{lstlisting}[caption={}] 10 | class M88kAsmPrinter : public AsmPrinter { 11 | public: 12 | explicit M88kAsmPrinter(TargetMachine &TM, 13 | std::unique_ptr 14 | Streamer) 15 | : AsmPrinter(TM, std::move(Streamer)) {} 16 | StringRef getPassName() const override 17 | { return "M88k Assembly Printer"; } 18 | 19 | void emitInstruction(const MachineInstr *MI) override; 20 | }; 21 | \end{lstlisting} 22 | 23 | 基本上,必须在emitInstruction()方法中处理两种不同的情况。MachineInstr实例仍然可以有操作数,这不是真正的机器指令,例如:对于RET操作码值的返回ret\underline{~}flag节点,就是这种情况。在M88k架构上,没有返回指令。所以,会跳转到r1寄存器中的地址存储。因此,当检测到RET操作码时,需要构造分支指令。默认情况下,降级只需要来自MachineInstr实例的信息,我们将这个任务委托给M88kMCInstLower类:\par 24 | 25 | \begin{lstlisting}[caption={}] 26 | void M88kAsmPrinter::emitInstruction(const MachineInstr *MI) { 27 | MCInst LoweredMI; 28 | switch (MI->getOpcode()) { 29 | case M88k::RET: 30 | LoweredMI = MCInstBuilder(M88k::JMP).addReg(M88k::R1); 31 | break; 32 | 33 | default: 34 | M88kMCInstLower Lower(MF->getContext(), *this); 35 | Lower.lower(MI, LoweredMI); 36 | break; 37 | } 38 | EmitToStreamer(*OutStreamer, LoweredMI); 39 | } 40 | \end{lstlisting} 41 | 42 | M88kMCInstLower类是没有预定义的超类。它的主要目的是处理各种操作数类型。由于目前只支持一组非常有限的操作数类型,可以将这个类简化为只有一个方法。lower()方法设置MCInst实例的操作码和操作数。只处理寄存器和立即操作数,其他操作数类型将会忽略。对于完整的实现,还需要处理内存地址:\par 43 | 44 | \begin{lstlisting}[caption={}] 45 | void M88kMCInstLower::lower(const MachineInstr *MI, MCInst 46 | &OutMI) const { 47 | OutMI.setOpcode(MI->getOpcode()); 48 | for (unsigned I = 0, E = MI->getNumOperands(); I != E; ++I) 49 | { 50 | const MachineOperand &MO = MI->getOperand(I); 51 | switch (MO.getType()) { 52 | case MachineOperand::MO_Register: 53 | if (MO.isImplicit()) 54 | break; 55 | OutMI.addOperand(MCOperand::createReg(MO.getReg())); 56 | break; 57 | 58 | case MachineOperand::MO_Immediate: 59 | OutMI.addOperand(MCOperand::createImm(MO.getImm())); 60 | break; 61 | 62 | default: 63 | break; 64 | } 65 | } 66 | } 67 | \end{lstlisting} 68 | 69 | 汇编打印器需要一个工厂方法,该方法在初始化时调用,例如:在InitializeAllAsmPrinters()中初始化:\par 70 | 71 | \begin{lstlisting}[caption={}] 72 | extern "C" LLVM_EXTERNAL_VISIBILITY void 73 | LLVMInitializeM88kAsmPrinter() { 74 | RegisterAsmPrinter X(getTheM88kTarget()); 75 | } 76 | \end{lstlisting} 77 | 78 | 最后,将指令降级到真正的机器码指令,但这还没有完成。我们需要对MC层的实现进行补全,这将在下一节中进行讨论。\par 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /content/3/chapter12/8.tex: -------------------------------------------------------------------------------- 1 | MC层负责以文本或二进制形式发出机器码。大多数功能要么在各种MC类中实现(只需要配置),要么从目标描述生成实现。\par 2 | 3 | MC层的初始化在MCTargetDesc/M88kMCTargetDesc.cpp中进行。以下类是在targeregistry单例中注册的:\par 4 | 5 | \begin{enumerate} 6 | \item M88kMCAsmInfo: 该类提供基本信息,如代码指针的大小、堆栈增长的方向、注释符号或汇编指令的名称。 7 | 8 | \item M88MCInstrInfo: 该类包含指令的信息,例如:指令名称。 9 | 10 | \item M88kRegInfo: 该类提供有关寄存器的信息,例如:寄存器的名称,或哪个寄存器是堆栈指针。 11 | 12 | \item M88kSubtargetInfo: 这个类保存调度模型的数据,以及解析和设置CPU特性的方法。 13 | 14 | \item M88kMCAsmBackend: 这个类提供了助手方法来获取与目标相关的重新定位数据。它还包含对象书写器类的工厂方法。 15 | 16 | \item M88kMCInstPrinter:该类包含以文本形式打印指令和操作数的助手方法。如果操作数在目标描述中定义了自定义打印方法,则要在该类中实现该方法。 17 | 18 | \item M88kMCCodeEmitter: 该类将指令的编码写入流。 19 | \end{enumerate} 20 | 21 | 根据后端实现的范围,我们不需要注册和实现所有这些类。如果不支持文本汇编器输出,可以忽略注册MCInstPrinter子类。如果不添加对写入对象文件的支持,可以省略MCAsmBackend和MCCodeEmitter子类。\par 22 | 23 | 文件开始包括生成的部分和提供它的工厂方法:\par 24 | 25 | \begin{lstlisting}[caption={}] 26 | #define GET_INSTRINFO_MC_DESC 27 | #include "M88kGenInstrInfo.inc" 28 | #define GET_SUBTARGETINFO_MC_DESC 29 | #include "M88kGenSubtargetInfo.inc" 30 | #define GET_REGINFO_MC_DESC 31 | #include "M88kGenRegisterInfo.inc" 32 | 33 | static MCInstrInfo *createM88kMCInstrInfo() { 34 | MCInstrInfo *X = new MCInstrInfo(); 35 | InitM88kMCInstrInfo(X); 36 | return X; 37 | } 38 | 39 | static MCRegisterInfo *createM88kMCRegisterInfo( 40 | const Triple &TT) { 41 | MCRegisterInfo *X = new MCRegisterInfo(); 42 | InitM88kMCRegisterInfo(X, M88k::R1); 43 | return X; 44 | } 45 | 46 | static MCSubtargetInfo *createM88kMCSubtargetInfo( 47 | const Triple &TT, StringRef CPU, StringRef 48 | FS) { 49 | return createM88kMCSubtargetInfoImpl(TT, CPU, FS); 50 | } 51 | \end{lstlisting} 52 | 53 | 我们还为在其他文件中实现的类提供了一些工厂方法:\par 54 | 55 | \begin{lstlisting}[caption={}] 56 | static MCAsmInfo *createM88kMCAsmInfo( 57 | const MCRegisterInfo &MRI, const Triple &TT, 58 | const MCTargetOptions &Options) { 59 | return new M88kMCAsmInfo(TT); 60 | } 61 | 62 | static MCInstPrinter *createM88kMCInstPrinter( 63 | const Triple &T, unsigned SyntaxVariant, 64 | const MCAsmInfo &MAI, const MCInstrInfo &MII, 65 | const MCRegisterInfo &MRI) { 66 | return new M88kInstPrinter(MAI, MII, MRI); 67 | } 68 | \end{lstlisting} 69 | 70 | 为了初始化MC层,只需要将所有的工厂方法注册到TargetRegistry单例中即可:\par 71 | 72 | \begin{lstlisting}[caption={}] 73 | extern "C" LLVM_EXTERNAL_VISIBILITY 74 | void LLVMInitializeM88kTargetMC() { 75 | TargetRegistry::RegisterMCAsmInfo(getTheM88kTarget(), 76 | createM88kMCAsmInfo); 77 | TargetRegistry::RegisterMCCodeEmitter(getTheM88kTarget(), 78 | createM88kMCCodeEmitter); 79 | TargetRegistry::RegisterMCInstrInfo(getTheM88kTarget(), 80 | createM88kMCInstrInfo); 81 | TargetRegistry::RegisterMCRegInfo(getTheM88kTarget(), 82 | createM88kMCRegisterInfo); 83 | TargetRegistry::RegisterMCSubtargetInfo(getTheM88kTarget(), 84 | createM88kMCSubtargetInfo); 85 | TargetRegistry::RegisterMCAsmBackend(getTheM88kTarget(), 86 | createM88kMCAsmBackend); 87 | TargetRegistry::RegisterMCInstPrinter(getTheM88kTarget(), 88 | createM88kMCInstPrinter); 89 | } 90 | \end{lstlisting} 91 | 92 | 另外,在MCTargetDesc/M88kTargetDesc.h头文件中,还需要包含生成的源文件的头文件部分:\par 93 | 94 | \begin{lstlisting}[caption={}] 95 | #define GET_REGINFO_ENUM 96 | #include "M88kGenRegisterInfo.inc" 97 | #define GET_INSTRINFO_ENUM 98 | 99 | #include "M88kGenInstrInfo.inc" 100 | #define GET_SUBTARGETINFO_ENUM 101 | #include "M88kGenSubtargetInfo.inc" 102 | \end{lstlisting} 103 | 104 | 我们将注册类的源文件全部放在MCTargetDesc目录中。对于第一个实现,只提供这些类的部分就足够了,例如:只要不将内存地址支持添加到目标描述中,就不会生成补丁。M88kMCAsmInfo类可以很快实现,只需要在构造函数中设置一些属性:\par 105 | 106 | \begin{lstlisting}[caption={}] 107 | M88kMCAsmInfo::M88kMCAsmInfo(const Triple &TT) { 108 | CodePointerSize = 4; 109 | IsLittleEndian = false; 110 | MinInstAlignment = 4; 111 | CommentString = "#"; 112 | } 113 | \end{lstlisting} 114 | 115 | 实现了MC层的支持类之后,就可以将机器码生成到文件中了。\par 116 | 117 | 下一节中,我们实现反汇编所需的类,这是反向的操作:将对象文件转换回汇编文本。\par 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /content/3/chapter12/9.tex: -------------------------------------------------------------------------------- 1 | 目标描述中指令的定义允许构造解码器表,解码器表用于将目标文件反汇编成文本汇编程序。解码器表和解码器函数是由llvm-tblgen工具生成的。除了生成的代码外,我们只需要提供注册和初始化M88kDisassembler类的代码,以及一些解码寄存器和操作数的帮助函数。\par 2 | 3 | 我们将实现放在Disassembler/M88kDisassembler.cpp文件中。M88kDisassembler类的get\allowbreak Instruction()方法执行解码工作。它接受一个字节数组作为输入,并将下一条指令解码到MCInst类的实例中。类声明如下:\par 4 | 5 | \begin{lstlisting}[caption={}] 6 | using DecodeStatus = MCDisassembler::DecodeStatus; 7 | 8 | namespace { 9 | 10 | class M88kDisassembler : public MCDisassembler { 11 | public: 12 | M88kDisassembler(const MCSubtargetInfo &STI, MCContext &Ctx) 13 | : MCDisassembler(STI, Ctx) {} 14 | ~M88kDisassembler() override = default; 15 | 16 | DecodeStatus getInstruction(MCInst &instr, uint64_t &Size, 17 | ArrayRef Bytes, 18 | uint64_t Address, 19 | raw_ostream &CStream) const 20 | override; 21 | }; 22 | } 23 | \end{lstlisting} 24 | 25 | 生成的类以非限定的方式引用DecodeStatus枚举,因此必须使名称可见。\par 26 | 27 | 为了初始化反汇编器,我们定义了一个工厂函数,只是实例化一个新对象:\par 28 | 29 | \begin{lstlisting}[caption={}] 30 | static MCDisassembler * 31 | createM88kDisassembler(const Target &T, 32 | const MCSubtargetInfo &STI, 33 | MCContext &Ctx) { 34 | return new M88kDisassembler(STI, Ctx); 35 | } 36 | \end{lstlisting} 37 | 38 | LLVMInitializeM88kDisassembler()函数中,将工厂函数注册到目标注册表中:\par 39 | 40 | \begin{lstlisting}[caption={}] 41 | extern "C" LLVM_EXTERNAL_VISIBILITY void 42 | LLVMInitializeM88kDisassembler() { 43 | TargetRegistry::RegisterMCDisassembler( 44 | getTheM88kTarget(), createM88kDisassembler); 45 | } 46 | \end{lstlisting} 47 | 48 | 当LLVM核心库初始化时,会调用initializealldisassemers()函数或InitializeNativeTarget\allowbreak Disassembler()函数。\par 49 | 50 | 生成的解码器函数需要助手函数来解码寄存器和操作数,这些元素的编码通常涉及目标描述中没有表示的特殊情况,例如:两个指令之间的距离总是偶数,所以最小的位可以忽略,因为它总是零。\par 51 | 52 | 要解码寄存器,必须定义DecodeGPRRegisterClass()函数。32个寄存器用0到31之间的数字进行编码,我们可以使用静态GPRDecoderTable表来映射编码和生成的寄存器枚举之间的关系:\par 53 | 54 | \begin{lstlisting}[caption={}] 55 | static const uint16_t GPRDecoderTable[] = { 56 | M88k::R0, M88k::R1, M88k::R2, M88k::R3, 57 | M88k::R4, M88k::R5, M88k::R6, M88k::R7, 58 | M88k::R8, M88k::R9, M88k::R10, M88k::R11, 59 | M88k::R12, M88k::R13, M88k::R14, M88k::R15, 60 | M88k::R16, M88k::R17, M88k::R18, M88k::R19, 61 | M88k::R20, M88k::R21, M88k::R22, M88k::R23, 62 | M88k::R24, M88k::R25, M88k::R26, M88k::R27, 63 | M88k::R28, M88k::R29, M88k::R30, M88k::R31, 64 | }; 65 | 66 | static DecodeStatus 67 | DecodeGPRRegisterClass(MCInst &Inst, uint64_t RegNo, 68 | uint64_t Address, 69 | const void *Decoder) { 70 | if (RegNo > 31) 71 | return MCDisassembler::Fail; 72 | 73 | unsigned Register = GPRDecoderTable[RegNo]; 74 | Inst.addOperand(MCOperand::createReg(Register)); 75 | return MCDisassembler::Success; 76 | } 77 | \end{lstlisting} 78 | 79 | 所有其他所需的解码器函数遵循与DecodeGPRRegisterClass()函数相同的模式:\par 80 | 81 | \begin{enumerate} 82 | \item 检查要解码的值是否符合所需的大小限制。如果不是,则返回MCDisassembler::Fail。 83 | 84 | \item 解码该值并将其添加到MCInst实例中。 85 | 86 | \item 返回MCDisassembler::Success表示成功。 87 | 88 | \end{enumerate} 89 | 90 | 然后,需要可以包括生成的解码器表和函数:\par 91 | 92 | \begin{lstlisting}[caption={}] 93 | #include "M88kGenDisassemblerTables.inc" 94 | \end{lstlisting} 95 | 96 | 最后,可以定义getInstruction()方法。这个方法有两个结果值,解码的指令和指令的大小。如果字节数组太小,则必须将大小设置为0。这很重要,因为即使解码失败,size参数也会将指针推进到下一个内存位置。\par 97 | 98 | 在M88k架构的情况下,方法很简单,因为所有指令都是4个字节长。因此,从数组中提取4个字节后,可以调用生成的解码器函数:\par 99 | 100 | \begin{lstlisting}[caption={}] 101 | DecodeStatus M88kDisassembler::getInstruction( 102 | MCInst &MI, uint64_t &Size, ArrayRef Bytes, 103 | uint64_t Address, raw_ostream &CS) const { 104 | if (Bytes.size() < 4) { 105 | Size = 0; 106 | return MCDisassembler::Fail; 107 | } 108 | Size = 4; 109 | 110 | uint32_t Inst = 0; 111 | for (uint32_t I = 0; I < Size; ++I) 112 | Inst = (Inst << 8) | Bytes[I]; 113 | return decodeInstruction(DecoderTableM88k32, MI, Inst, 114 | Address, this, STI); 115 | } 116 | \end{lstlisting} 117 | 118 | 这就完成了反汇编程序的实现。\par 119 | 120 | 在实现了所有类之后,只需要设置构建系统来获取新的目标后端,我们将在下一节中添加它。\par 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /content/3/chapter9/0.tex: -------------------------------------------------------------------------------- 1 | 到目前为止使用的LLVM IR仍然需要转换成机器指令,这称为\textbf{指令选择},通常缩写为\textbf{ISel}。指令选择是后端的重要组成部分,LLVM有三种不同的指令选择方法:选择DAG、快速指令选择和全局指令选择。\par 2 | 3 | 在本章中,您将学习以下内容:\par 4 | 5 | \begin{itemize} 6 | \item 了解LLVM目标后端结构,将介绍目标后端执行的任务,并检查要运行的机器。 7 | \item 使用\textbf{机器IR(MIR)}来测试和调试后端,这有助于您输出MIR后指定的通过和运行一个通过的MIR文件。 8 | \item 指令选择是如何工作的,在其中您将了解LLVM执行指令选择的不同方式。 9 | \item 支持新的机器指令,在其中添加一个新的机器指令,并使其用于指令选择。 10 | \end{itemize} 11 | 12 | 在本章结束时,您将了解目标后端是如何构造的,以及指令选择是如何工作的。您还将获得将当前不支持的机器指令添加到汇编程序和指令选择的知识,以及如何测试您添加的指令。\par 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /content/3/chapter9/1.tex: -------------------------------------------------------------------------------- 1 | 要查看图形可视化,必须安装Graphviz软件,可以从Graphviz官网下载该软件\url{https://graphviz.org/}. 源代码可在 \url{http://gitlab.com/graphviz/graphviz/}获取。\par 2 | 3 | 本章的代码文件可在\url{https://github.com/PacktPublishing/Learn-LLVM-12/tree/master/Chapter09}获取。\par 4 | 5 | 你可以在视频中找到代码\url{https://bit.ly/3nllhED}。\par -------------------------------------------------------------------------------- /content/3/chapter9/2.tex: -------------------------------------------------------------------------------- 1 | 优化LLVM IR后,使用所选的LLVM目标生成机器码。其中,以下任务在后端执行:\par 2 | 3 | \begin{enumerate} 4 | \item 构造了用于指令选择的有向无环图(DAG),通常称为SelectionDAG。 5 | \item 选择与IR代码相对应的机器指令。 6 | \item 选定的机器指令按最优顺序排列。 7 | \item 机器寄存器取代虚拟寄存器。 8 | \item 函数中添加头尾代码。 9 | \item 按最优顺序排列基本块。 10 | \item 运行目标特定的Pass。 11 | \item 使用对象源码或汇编进行触发。 12 | \end{enumerate} 13 | 14 | 所有这些步骤都是由MachineFunctionPass类派生的机器函数Pass来实现的。这是FunctionPass类的一个子类,该类是旧Pass管理器使用的基类之一。在LLVM 12中,机器功能Pass到新Pass管理器的转换仍在进行中。\par 15 | 16 | 在所有这些步骤中,LLVM指令都要进行转换。在代码层,一个LLVM IR指令由Instruction类的实例表示。在指令选择阶段,它转换为MachineInstr实例。这是一个更接近实际机器表示。它已经包含了对目标有效的指令,但仍然在虚拟寄存器上操作(到寄存器分配),还可以包含某些伪指令。指令选择之后的传递会对其进行改进,最后创建MCInstr实例,它是真实机器指令的表示。MCInstr实例可以写入目标文件或打印为汇编代码。\par 17 | 18 | 为了探索后端Pass,可以创建一个包含以下内容的小IR文件:\par 19 | 20 | \begin{tcolorbox}[colback=white,colframe=black] 21 | define i16 @sum(i16 \%a, i16 \%b) \{ \\ 22 | \hspace*{0.5cm}\%res = add i16 \%a, 3 \\ 23 | \hspace*{0.5cm}ret i16 \%res \\ 24 | \} 25 | \end{tcolorbox} 26 | 27 | 将此代码保存为sum.ll,使用llc(LLVM静态编译器)编译MIPS架构。该工具将LLVM IR编译为汇编文本或目标文件。可以在命令行中用-mtriple选项覆盖编译的目标平台。使用-debug-pass=\allowbreak Structure选项调用llc工具:\par 28 | 29 | \begin{tcolorbox}[colback=white,colframe=black] 30 | \$ llc -mtriple=mips-linux-gnu -debug-pass=Structure < sum.ll 31 | \end{tcolorbox} 32 | 33 | 除了生成的程序集代码之外,您还将看到一长串要运行的机器Pass列表。其中,MIPS DAG->DAG指令选择模式Pass执行指令选择,MIPS延迟槽填充器是针对特定目标的Pass,清理前的最后一个Pass是MIPS汇编打印器负责打印汇编代码。在所有这些Pass中,指令选择Pass是最有趣的,我们将在下一节详细讨论它。\par 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /content/3/chapter9/3.tex: -------------------------------------------------------------------------------- 1 | 在上一节中,您看到许多Pass在后端运行。然而,这些Pass中的大多数不是在LLVM IR上运行,而是在MIR上运行。这是依赖于目标的指令表示,因此比LLVM IR更低层。它仍可以包含对虚拟寄存器的引用,因此它还不是纯CPU指令。\par 2 | 3 | 例如,要查看IR级别的优化,可以告诉llc在每个Pass完成后转储IR。因为它们不在IR上工作,所以并不适用于后台的机器Pass。不过,MIR也有类似的功能。\par 4 | 5 | MIR是当前模块中机器指令当前状态的文本表示。它利用了YAML格式,允许序列化和反序列化。基本思想是,可以在某个点停止传递Pass,并以YAML格式检查状态。您还可以修改YAML文件,或者创建您自己的YAML文件,传递它,并检查结果。这样可以方便地调试和测试。\par 6 | 7 | 让我们看一下MIR。使用\verb|--|stop-after=finalize-iseloption和我们之前使用的测试输入文件运行llc工具:\par 8 | 9 | \begin{tcolorbox}[colback=white,colframe=black] 10 | \$ llc -mtriple=mips-linux-gnu $\setminus$ \\ 11 | \hspace*{2cm}-stop-after=finalize-isel < sum.ll 12 | \end{tcolorbox} 13 | 14 | 这指示llc在指令选择完成后转储MIR。缩短后的输出如下所示:\par 15 | 16 | \begin{tcolorbox}[colback=white,colframe=black] 17 | \verb|---| \\ 18 | name: \hspace{3cm}sum \\ 19 | body: \hspace{3cm}| \\ 20 | \hspace*{0.5cm}bb.0 (\%ir-block.0): \\ 21 | \\ 22 | \hspace*{0.5cm}liveins: \$a0, \$a1 \\ 23 | \hspace*{0.5cm}\%1:gpr32 = COPY \$a1 \\ 24 | \hspace*{0.5cm}\%0:gpr32 = COPY \$a0 \\ 25 | \hspace*{0.5cm}\%2:gpr32 = ADDu \%0, \%1 \\ 26 | \hspace*{0.5cm}\$v0 = COPY \%2 \\ 27 | \hspace*{0.5cm}RetRA implicit \$v0 \\ 28 | ... 29 | 30 | \end{tcolorbox} 31 | 32 | 您可以注意几个属性。首先,虚拟寄存器(如\%0)和真实机器寄存器(如\$a0)的混合使用,原因在使用了更底层的ABI。为了跨不同的编译器和语言移植,函数遵循调用约定,这是应用程序二进制接口(ABI)的一部分。该输出用于MIPS机器上的Linux系统,需要使用系统使用的调用规则,第一个参数在寄存器\$a0中传递。MIR输出是在指令选择之后,但在寄存器分配之前生成的,所以仍然可以看到虚拟寄存器的使用。\par 33 | 34 | MIR文件中使用的是机器指令ADDu,而不是LLVM IR中的add指令。您还可以看到虚拟寄存器附加了一个寄存器调用,本例中是gpr32。MIPS体系结构上没有16位寄存器,因此必须使用32位寄存器。\par 35 | 36 | bb.0标签为第一个基本块,标签后的缩进内容是基本块的一部分。第一个语句指定在进入基本块时处于活动状态的寄存器。在本例中,只有\$a0和\$a1这两个参数在输入时是活动的。\par 37 | 38 | MIR文件中还有很多其他细节可以在LLVM MIR文档中进行了解 \url{https://llvm.org/docs/MIRLangRef.html}。\par 39 | 40 | 遇到的第一个问题可能是如何找到一个Pass的名称,特别是当只需要检查该Pass之后的输出而不主动处理时。当使用带有llc的-debug-pass=Structure选项时,激活Pass的选项会打印在顶部。如果想在Mips延迟槽填充器Pass之前停止,那么您需要查看打印的列表,并找到-mipsdelay-slot-filler选项,其也代表相应Pass的名称。\par 41 | 42 | MIR文件格式的主要应用是在后端辅助测试机通过。使用llc和\verb|--|stop-after选项,在指定Pass后得到MIR。通常,使用它作为预期测试用例的基础。您要注意的第一件事是,MIR输出非常冗长。例如,许多字段是空的。为了减少这种混乱,可以在llc命令行中添加-simplify-mir选项。\par 43 | 44 | 您可以根据测试用例的需要保存和更改MIR。llc工具可以运行一个Pass,这是与MIR文件测试的完美匹配。我们假设您想要测试MIPS延迟槽填充器Pass。延迟槽是RISC体系结构(如MIPS或SPARC)的一个特殊属性:跳转后的下一条指令总是是执行。因此,编译器必须确保在每次跳转后都有合适的指令,而这个Pass就是执行这个任务的。\par 45 | 46 | 在运行Pass之前生成MIR:\par 47 | 48 | \begin{tcolorbox}[colback=white,colframe=black] 49 | \$ llc -mtriple=mips-linux-gnu $\setminus$ \\ 50 | \hspace*{2cm}-stop-before=mips-delay-slot-filler -simplify-mir $\setminus$ \\ 51 | \hspace*{2cm}< sum.ll >delay.mir 52 | \end{tcolorbox} 53 | 54 | 输出要小很多,因为使用了-simplify-mir选项。函数体如下所示:\par 55 | 56 | \begin{tcolorbox}[colback=white,colframe=black] 57 | body: \hspace{3cm} | \\ 58 | \hspace*{0.5cm}bb.0 (\%ir-block.0): \\ 59 | \hspace*{1cm}liveins: \$a0, \$a1 \\ 60 | \\ 61 | \hspace*{1cm}renamable \$v0 = ADDu killed renamable \$a0, \\ 62 | \hspace*{6cm}killed renamable \$a1 \\ 63 | \hspace*{1cm}PseudoReturn undef \$ra, implicit \$v0 64 | \end{tcolorbox} 65 | 66 | 最值得注意的是,将看到ADDu指令后面是用于返回的apseudo指令。\par 67 | 68 | delay.ll文件作为输入,现在运行延迟槽填充器Pass:\par 69 | 70 | \begin{tcolorbox}[colback=white,colframe=black] 71 | \$ llc -mtriple=mips-linux-gnu $\setminus$ \\ 72 | \hspace*{2cm}-run-pass=mips-delay-slot-filler -o - delay.mir 73 | \end{tcolorbox} 74 | 75 | 现在将输出的函数与之前的函数进行比较:\par 76 | 77 | \begin{tcolorbox}[colback=white,colframe=black] 78 | body: \hspace{3cm} | \\ 79 | \hspace*{0.5cm}bb.0 (\%ir-block.0): \\ 80 | \hspace*{1cm}PseudoReturn undef \$ra, implicit \$v0 \{ \\ 81 | \hspace*{1.5cm}renamable \$v0 = ADDu killed renamable \$a0, \\ 82 | \hspace*{6cm}killed renamable \$a1 83 | \end{tcolorbox} 84 | 85 | 您可以看到,用于返回的ADDu和伪指令已经更改了顺序,ADDu指令现在嵌套在返回语句中:传递标识了适合于延迟槽的ADDu指令。\par 86 | 87 | 如果您刚接触到延迟槽的概念,也会想要看看生成的组件,这可以通过llc很轻松的完成:\par 88 | 89 | \begin{tcolorbox}[colback=white,colframe=black] 90 | \$ llc -mtriple=mips-linux-gnu < sum.ll 91 | \end{tcolorbox} 92 | 93 | 输出包含了很多细节,但是在bb.0基础块的帮助下,可以很容易地找到为它生成的汇编代码:\par 94 | 95 | \begin{tcolorbox}[colback=white,colframe=black] 96 | \# \%bb.0: \\ 97 | \hspace*{2cm}jr\hspace{1cm} \$ra \\ 98 | \hspace*{2cm}jaddu\hspace{0.5cm} \$2, \$4, \$5 99 | \end{tcolorbox} 100 | 101 | 的确,指令的顺序改变了!\par 102 | 103 | 有了这些知识,我们将了解后端的核心,并检查如何在LLVM中执行机器指令的选择。\par 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /content/3/chapter9/5.tex: -------------------------------------------------------------------------------- 1 | 您的目标CPU可能有LLVM还不支持的机器指令。例如,使用MIPS架构的制造商经常向核心MIPS指令集添加特殊指令。RISC-V指令集的规范明确允许制造商添加新的指令,或者要添加一个全新的后端,然后必须添加CPU指令。在下一节中,我们将为LLVM后端添加对单个新机器指令的汇编器支持。\par 2 | 3 | \hspace*{\fill} \par %插入空行 4 | \textbf{向汇编程序和代码生成添加新指令} 5 | 6 | 新的机器指令通常与特定的CPU特性绑定在一起。只有当用户使用\verb|--|mattr=选项选择llc时,新指令才能识别。\par 7 | 8 | 作为一个例子,我们将添加一个新的机器指令到MIPS后端。新的机器指令首先将两个输入寄存器的值平方\$2和\$3,然后将两个平方和的和赋给输出寄存器\$1:\par 9 | 10 | \begin{tcolorbox}[colback=white,colframe=black] 11 | sqsumu \$1, \$2, \$3 12 | \end{tcolorbox} 13 | 14 | 该指令的名称是sqsumu,由平方和求和运算派生而来。名称中的最后一个u表示该指令适用于无符号整数。\par 15 | 16 | 我们首先添加的CPU特性称为sqsum。这将允许使用\verb|--|mattr=+sqsum选项调用llc,以启用对新指令进行识别。\par 17 | 18 | 我们将添加的大部分代码都在描述MIPS后端的TableGen文件中。所有文件都位于llvm/lib/\allowbreak Target/Mips文件夹中。顶层文件为Mips.td,查看该文件并找到定义各种特性的部分。这里添加了我们新特性的定义:\par 19 | 20 | \begin{tcolorbox}[colback=white,colframe=black] 21 | def FeatureSQSum \\ 22 | \hspace*{1cm}: SubtargetFeature<"sqsum", "HasSQSum", "true", \\ 23 | \hspace*{6cm}"Use square-sum instruction">; 24 | \end{tcolorbox} 25 | 26 | SubtargetFeature类接受四个模板参数。第一个是sqsum,它是特性的名称,用于命令行。第二个参数HasSQSum是表示此特性的Subtarget类中的属性名称。下一个参数是默认值和特性的描述,用于在命令行上提供帮助。TableGen为MipsSubtarget类生成基类,其在MipsSubtarget.h文件中定义。这个文件中,类的私有部分添加了新属性,所有其他属性都在这里定义:\par 27 | 28 | \begin{lstlisting}[caption={}] 29 | // Has square-sum instruction. 30 | bool HasSQSum = false; 31 | \end{lstlisting} 32 | 33 | 在public部分,还使用了一个方法来检索属性的值:\par 34 | 35 | \begin{lstlisting}[caption={}] 36 | bool hasSQSum() const { return HasSQSum; } 37 | \end{lstlisting} 38 | 39 | 通过这些那方法,我们已经能够在命令行上设置sqsum特性,尽管目前还没有效果。\par 40 | 41 | 要将新指令绑定到sqsum特性,需要定义一个谓词来指示是否选择了该特性。我们将其添加到MipsInstrInfo.td文件中,或者在定义所有其他谓词的部分,亦或者简单地放在结尾:\par 42 | 43 | \begin{tcolorbox}[colback=white,colframe=black] 44 | def HasSQSum : Predicate<"Subtarget->hasSQSum()">, \\ 45 | \hspace*{6cm}AssemblerPredicate<(all\underline{~}of FeatureSQSum)>; 46 | \end{tcolorbox} 47 | 48 | 谓词使用前面定义的hasSQSum()方法。此外,AssemblerPredicate模板指定为汇编程序生成源代码时所使用的条件,只是简单地引用前面定义的特性。\par 49 | 50 | 我们还需要更新调度模型。MIPS目标同时使用行程和机器指令调度程序,对于路线模型,为MipsSchedule中的每条指令定义一个instritclass.td记录文件。在此文件中定义所有路线的部分中添加以下行:\par 51 | 52 | \begin{tcolorbox}[colback=white,colframe=black] 53 | def II\underline{~}SQSUMU : InstrItinClass; 54 | \end{tcolorbox} 55 | 56 | 我们还需要提供说明费用的细节。通常,可以在CPU的文档中找到这些信息。对于我们的指令,乐观地假设它在ALU中只需要一个循环。该信息能添加到的MipsGenericItineraries定义中:\par 57 | 58 | \begin{tcolorbox}[colback=white,colframe=black] 59 | InstrItinData]> 60 | \end{tcolorbox} 61 | 62 | 这样,对基于路线的调度模型的更新就完成了。MIPS目标还在MipsScheduleGeneric.td文件中,定义了基于MipsScheduleGeneric中的机器指令调度器模型的通用调度模型。因为这是一个包含所有指令的完整模型,所以还需要添加指令add。因为它是基于乘法的,所以我们简单地扩展了MULT和MULTu指令的定义:\par 63 | 64 | \begin{tcolorbox}[colback=white,colframe=black] 65 | def : InstRW<[GenericWriteMul], (instrs MULT, MULTu, SQSUMu)>; 66 | \end{tcolorbox} 67 | 68 | MIPS目标还在MipsScheduleP5600.td中定义了P5600 CPU的调度模型。这个目标显然不支持我们的新指令,所以把它添加到不支持的特性列表中:\par 69 | 70 | \begin{tcolorbox}[colback=white,colframe=black] 71 | list UnsupportedFeatures = [HasSQSum, HasMips3, … 72 | \end{tcolorbox} 73 | 74 | 现在,我们准备在Mips64InstrInfo.td文件的末尾添加新指令。TableGen的定义总是简洁的,因此需要对其进行剖析。该定义使用了来自MIPS目标描述的一些预定义类,我们的新指令是一个算术指令。通过设计,它适合于ArithLogicR类。第一个参数sqsumu指定指令的汇编助记符,下一个参数GPR64Opnd表示指令使用64位寄存器作为操作数,下面的1个参数表示操作数是可交换的。最后,给出了路线指令。添加\underline{~}FM类用于指定指令的二进制编码。对于真正的指令,必须根据文档选择参数。然后跟随ISA\underline{~}MIPS64谓词,该谓词指示指令对哪个指令集是有效的。最后,我们的SQSUM声明,只有在启用我们的特性时,指令才有效。完整的定义如下:\par 75 | 76 | \begin{tcolorbox}[colback=white,colframe=black] 77 | def SQSUMu : ArithLogicR<"sqsumu", GPR64Opnd, 1, II\underline{~}SQSUMU>, \\ 78 | \hspace*{4cm}ADD\underline{~}FM<0x1c, 0x28>, ISA\underline{~}MIPS64, SQSUM 79 | \end{tcolorbox} 80 | 81 | 如果您的目标只是支持新指令,那么这个定义就够了,所以一定要完成的定义。通过添加选择DAG模式,代码生成器可以使用该指令。该指令使用两个操作数寄存器\$rs和\$rt以及目标寄存器\$rd,这三个寄存器都由ADD\underline{~}FM二进制格式类定义。理论上,要匹配的模式很简单:使用乘数运算符将每个寄存器的值平方,然后使用add运算符将两个乘积相加,并将它们赋给目标寄存器\$rd。这个模式变得有点复杂,因为使用MIPS指令集,乘法的结果存储在特殊的寄存器对中。为了便于使用,结果必须移到通用寄存器中。在操作的合法化过程中,通用mul操作符被MIPS特定的MipsMult操作所取代,用于乘法运算和MipsMFLO运算,以将结果的下一部分移动到通用寄存器中。在编写模式时,必须考虑到这一点,模式如下所示:\par 82 | 83 | \begin{tcolorbox}[colback=white,colframe=black] 84 | \{ 85 | \hspace*{0.5cm}let Pattern = [(set GPR64Opnd:\$rd, \\ 86 | \hspace*{4cm}(add (MipsMFLO (MipsMult \\ 87 | \hspace*{4.5cm}GPR64Opnd:\$rs, \\ 88 | \\ 89 | \hspace*{4.5cm}GPR64Opnd:\$rs)), \\ 90 | \hspace*{5.5cm}(MipsMFLO (MipsMult \\ 91 | \hspace*{6cm}GPR64Opnd:\$rt, \\ 92 | \\ 93 | \hspace*{6cm}GPR64Opnd:\$rt))) \\ 94 | \hspace*{4.5cm})];\\ 95 | \} 96 | \end{tcolorbox} 97 | 98 | 正如在带选择DAG部分的指令选择中所描述的,如果该模式与当前DAG节点匹配,则选择我们的新指令。由于SQSUM谓词,这只在激活SQSUM特性时发生。我们用一个测试来检查它!\par 99 | 100 | \hspace*{\fill} \par %插入空行 101 | \textbf{测试新指令} 102 | 103 | 如果扩展了LLVM,那么最好使用自动化测试来验证它。如果想将您的扩展贡献给LLVM项目,那么就需要良好的测试。\par 104 | 105 | 在像上一节一样添加一个新的机器指令之后,我们必须进行两个检查:\par 106 | 107 | \begin{itemize} 108 | \item 首先,必须验证指令编码是否正确。 109 | \item 其次,必须确保代码生成按照预期工作。 110 | \end{itemize} 111 | 112 | LLVM项目使用LIT(LLVM Integrated Tester)作为测试工具。基本上,测试用例是一个包含输入、要运行的命令和应该执行的检查的文件。添加新测试就像将一个新文件复制到测试目录中一样简单。为了验证新指令的编码,使用llvm-mc工具。除了其他任务外,该工具还可以显示指令的编码。对于临时检查,可以执行以下命令显示编码指令:\par 113 | 114 | \begin{tcolorbox}[colback=white,colframe=black] 115 | \$ echo "sqsumu $\setminus$\$1,$\setminus$\$2,$\setminus$\$3" | $\setminus$ \\ 116 | \hspace*{0.5cm}llvm-mc \verb|--|triple=mips64-linux-gnu -mattr=+sqsum $\setminus$ \\ 117 | \hspace*{2.5cm}--show-encoding 118 | \end{tcolorbox} 119 | 120 | 这已经显示了要在自动化测试用例中运行的部分输入和命令。要验证结果,可以使用FileCheck工具,llvm-mc的输出通过管道传输到这个工具中。另外,FileCheck读取测试用例文件。测试用例文件包含用CHECK:关keyword标记的行,其后是预期的输出。FileCheck尝试将这些行与传入它的数据进行匹配。如果没有找到匹配,则显示一个错误。将包含测试用例文件sqsumu.s放到llvm/test/MC/Mips目录中:\par 121 | 122 | \begin{tcolorbox}[colback=white,colframe=black] 123 | \# RUN: llvm-mc \%s -triple=mips64-linux-gnu -mattr=+sqsum $\setminus$ \\ 124 | \# RUN: \verb|--|show-encoding | FileCheck \%s \\ 125 | \# CHECK: sqsumu \$1, \$2, \$3 \# encoding: [0x70,0x43,0x08,0x28] \\ 126 | \\ 127 | \hspace*{1cm}sqsumu \$1, \$2, \$3 128 | \end{tcolorbox} 129 | 130 | 如果位于llvm/test/Mips/MC文件夹中,那么可以使用以下命令运行测试,并在最后报告成功:\par 131 | 132 | \begin{tcolorbox}[colback=white,colframe=black] 133 | \$ llvm-lit sqsumu.s \\ 134 | \verb|--| Testing: 1 tests, 1 workers \verb|--| \\ 135 | PASS: LLVM :: MC/Mips/sqsumu.s (1 of 1) \\ 136 | Testing Time: 0.11s \\ 137 | \hspace*{0.5cm}Passed: 1 138 | \end{tcolorbox} 139 | 140 | LIT工具解释RUN:line,用当前文件名替换\%s。FileCheck工具读取文件,并解析CHECK:line,尝试匹配来自流水的输入。这是一种非常有效的测试方法。\par 141 | 142 | 如果位于构建目录中,则可以使用以下命令调用LLVM测试:\par 143 | 144 | 要为代码生成构建一个测试用例,需要遵循相同的策略。sqsum.ll文件包含计算斜边平方的LLVM IR代码:\par 145 | 146 | \begin{tcolorbox}[colback=white,colframe=black] 147 | define i64 @hyposquare(i64 \%a, i64 \%b) \{ \\ 148 | \hspace*{0.5cm}\%asq = mul i64 \%a, \%a \\ 149 | \hspace*{0.5cm}\%bsq = mul i64 \%b, \%b \\ 150 | \hspace*{0.5cm}\%res = add i64 \%asq, \%bsq \\ 151 | \hspace*{0.5cm}ret i64 \%res \\ 152 | \} 153 | \end{tcolorbox} 154 | 155 | 要查看生成的汇编代码,可以使用llc工具:\par 156 | 157 | \begin{tcolorbox}[colback=white,colframe=black] 158 | \$ llc –mtriple=mips64-linux-gnu –mattr=+sqsum < sqsum.ll 159 | \end{tcolorbox} 160 | 161 | 确信在输出中看到了新的sqsum指令。如果删除-mattr=+sqsum选项,还请检查该指令是否未生成。\par 162 | 163 | 有了这些,就可以构建测试用例了。这一次,使用两个RUN:line:一行检查新指令是否生成,另一行检查是否没有生成。我们可以用一个测试用例文件做到这两点,因为可以告诉FileCheck工具寻找与“CHECK:”不同的标签。将测试文件sqsum.ll和以下内容放置到llvm/test/CodeGen/Mips文件夹:\par 164 | 165 | \begin{tcolorbox}[colback=white,colframe=black] 166 | ; RUN: llc -mtriple=mips64-linux-gnu -mattr=+sqsum < \%s |$\setminus$ \\ 167 | ; RUN: FileCheck -check-prefix=SQSUM \%s \\ 168 | ; RUN: llc -mtriple=mips64-linux-gnu < \%s |$\setminus$ \\ 169 | ; RUN: FileCheck --check-prefix=NOSQSUM \%s \\ 170 | \\ 171 | define i64 @hyposquare(i64 \%a, i64 \%b) \{ \\ 172 | ; SQSUM-LABEL: hyposquare: \\ 173 | ; SQSUM: sqsumu \$2, \$4, \$5 \\ 174 | ; NOSQSUM-LABEL: hyposquare: \\ 175 | ; NOSQSUM: dmult \$5, \$5 \\ 176 | ; NOSQSUM: mflo \$1 \\ 177 | ; NOSQSUM: dmult \$4, \$4 \\ 178 | ; NOSQSUM: mflo \$2 \\ 179 | ; NOSQSUM: addu \$2, \$2, \$1 \\ 180 | \hspace*{0.5cm}\%asq = mul i64 \%a, \%a \\ 181 | \hspace*{0.5cm}\%bsq = mul i64 \%b, \%b \\ 182 | \hspace*{0.5cm}\%res = add i64 \%asq, \%bsq \\ 183 | \hspace*{0.5cm}ret i64 \%res \\ 184 | \} 185 | \end{tcolorbox} 186 | 187 | 与其他测试一样,可以使用以下命令在文件夹中单独运行测试:\par 188 | 189 | \begin{tcolorbox}[colback=white,colframe=black] 190 | \$ llvm-lit squm.ll 191 | \end{tcolorbox} 192 | 193 | 或者,可以使用以下命令从构建目录运行: 194 | 195 | \begin{tcolorbox}[colback=white,colframe=black] 196 | \$ ninja check-llvm-mips-codegen 197 | \end{tcolorbox} 198 | 199 | 通过这些步骤,可以使用新指令增强LLVM汇编程序,允许指令选择使用这个新指令,并验证编码是否正确,是否按照预期完成代码生成的工作。\par 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | -------------------------------------------------------------------------------- /content/3/chapter9/6.tex: -------------------------------------------------------------------------------- 1 | 本章中,您学习了LLVM目标的后端是如何构造的。使用MIR检查通过后的状态,并使用机器IR运行单次通过。有了这些知识,您就可以研究后端传递中的问题。\par 2 | 3 | 了解了如何在LLVM中使用选择DAG实现指令选择,还介绍了使用FastISel和GlobalISel进行指令选择的替代方法,如果平台提供了所有这些方法,这有助于决定选择哪一种算法。\par 4 | 5 | 您扩展了LLVM以在汇编程序和指令选择中支持新的机器指令,这有助于添加对当前不受支持的CPU特性的支持。为了验证扩展,需要为它添加自动化测试用例。\par 6 | 7 | 下一章中,我们将研究LLVM的另一个独特特性:一步生成和执行代码,也称为JIT编译。\par 8 | 9 | \newpage -------------------------------------------------------------------------------- /content/3/chapter9/images/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/content/3/chapter9/images/1.jpg -------------------------------------------------------------------------------- /content/Preface.tex: -------------------------------------------------------------------------------- 1 | \begin{flushright} 2 | \zihao{0} 前言 3 | \end{flushright} 4 | 5 | 构造编译器是一项复杂而有趣的任务。LLVM为编译器提供了可重用组件,其核心库实现了一个世界级的优化代码生成器,它为所有主流CPU架构转换代码提供了独立的中间表示。许多编译器已经在使用LLVM的相关技术了。\par 6 | 7 | 这本书会教您如何实现自己的编译器,并通过LLVM实现它。您将了解前端编译器如何将源代码转换为抽象语法树,以及如何从中生成中间表示(IR)。如何向编译器添加优化流水,对IR进行优化,并编译为性能不错的机器代码。\par 8 | 9 | LLVM框架可以通过多种方式进行扩展,您将学习如何添加新的通道、新的机器指令,甚至是全新的LLVM后端。还有高级主题,如编译不同的CPU架构和扩展Clang、Clang静态分析器与插件和检查器也涵盖其中。本书遵循最实用的方法,并附带了示例源代码,这可以让您更容易得在项目中应用所学习到的知识。\par 10 | 11 | \hspace*{\fill} \par %插入空行 12 | \textbf{适宜读者} 13 | 14 | 本书为刚接触LLVM并且对LLVM框架感兴趣的编译器开发人员、爱好者和工程师编写。对于希望使用基于编译器的工具进行代码分析和改进的C++软件工程师,以及希望获得更多LLVM基本知识的LLVM库的用户也很有用。为了更有效地理解本书中所包含的概念,必须具备C++中级水平。\par 15 | 16 | \hspace*{\fill} \par %插入空行 17 | \textbf{本书内容} 18 | 19 | \textit{第1章,安装LLVM},介绍如何设置开发环境。了解如何编译了LLVM库,并学习了如何自定义构建过程。\par 20 | 21 | \textit{第2章,浏览LLVM},介绍各种LLVM项目,并讨论所有项目的统一目录结构。您将使用LLVM核心库创建第一个项目,还可以为不同的CPU架构编译它。\par 22 | 23 | \textit{第3章,编译器结构},提供编译器组件的概述。您将实现可以生成LLVM IR的编译器。\par 24 | 25 | \textit{第4章,将源码转换为抽象语法树},详细地介绍了如何实现编译器前端。您将为小型编程语言创建前端,最后构建抽象语法树。\par 26 | 27 | \textit{第5章,生成IR——基础知识},展示了如何从抽象语法树生成LLVM IR。本章的最后,您将为示例语言实现编译器,从而产生汇编文本或目标代码文件。\par 28 | 29 | \textit{第6章,生成高级语言结构的IR},介绍了如何将高级编程语言中常见的源语言特性转换为LLVM IR。您将了解聚合数据类型的转换、实现类继承和虚函数的各种选项,以及如何遵循系统的ABI。\par 30 | 31 | \textit{第7章,生成IR——进阶知识},介绍了如何在源语言中为异常处理语句生成LLVM IR。了解如何为基于类型的别名分析添加元数据,以及如何向生成的LLVM IR添加调试信息,并且您将扩展编译器生成的元数据。\par 32 | 33 | \textit{第8章,优化IR},介绍LLVM Pass管理器。您将实现你自己的Pass,作为LLVM的一部分或者插件,您将学习如何添加新Pass到优化流水中。\par 34 | 35 | \textit{第9章,选择指令},介绍了LLVM如何减少生成机器指令的IR。您将学习如何在LLVM中定义指令,并将向LLVM添加一条新的机器指令,以便在选择指令时使用新指令。\par 36 | 37 | \textit{第10章,JIT编译},讨论如何使用LLVM来实现JIT编译器。本章结束时,您将通过两种不同的方式实现自己的LLVM IR JIT编译器。\par 38 | 39 | \textit{第11章,使用LLVM工具调试},探索了LLVM的各种库和组件,这可以帮助您了解应用程序中的bug。您将使用“杀虫剂”来识别内存溢出和其他错误。使用libFuzzer库,将随机数据作为输入测试函数。XRay也能帮助您找到性能瓶颈。您将使用Clang静态分析程序来识别源代码级的bug,并且可以将自己的检查程序添加到分析程序中。您还将学习如何使用自己的插件扩展Clang。\par 40 | 41 | \textit{第12章,自定义编译器后端},说明如何向LLVM添加新的后端。需要实现所有必要的类。在本章的最后,将为某种CPU架构编译LLVM IR。\par 42 | 43 | \hspace*{\fill} \par %插入空行 44 | \textbf{编译环境} 45 | 46 | \textit{需要一台运行Linux、Windows、macOS或FreeBSD的计算机,并为该操作系统安装了开发工具链。所需工具见表。所有工具都应该在shell的搜索路径中。}\par 47 | 48 | \begin{table}[H] 49 | \begin{tabular}{|l|l|} 50 | \hline 51 | 书中涉及的软件/硬件 & 操作系统 \\ \hline 52 | \begin{tabular}[c]{@{}l@{}} C/C++编译器:gcc 5.1.0或更高版本, clang3.5或更高版本\\ Apple clang 6.0或更高版本, Visual Studio 2017或更高版本\end{tabular} & \begin{tabular}[c]{@{}l@{}}Linux(任意衍生版), Windows,\\ macOS,或FreeBSD\end{tabular} \\ \hline 53 | CMake 3.13.4或更高版本 & \\ \hline 54 | Ninja 1.9.0 & \\ \hline 55 | Python 3.6或更高版本 & \\ \hline 56 | Git 1.7.10或更高版本 & \\ \hline 57 | \end{tabular} 58 | \end{table} 59 | 60 | 第9章“指令选择”中的DAG可视化,必须安装\url{https://graphviz.org/}的Graphviz。默认情况下,生成的图像是PDF格式的,所以需要PDF查看器来显示。\par 61 | 62 | 第11章“使用LLVM工具调试”中创建“火焰图”,需要从\url{https://github.com/brendangregg/FlameGraph}获取安装脚本。要运行该脚本,还需要安装最新版本的Perl,要查看图形,还需要一个能够显示SVG文件的Web浏览器(主流浏览器都没问题)。要在同一章中看到Chrome跟踪查看器可视化,需要安装Chrome浏览器。\par 63 | 64 | \textbf{如果正在使用本书的数字版本,我们建议您自己输入代码或通过GitHub存储库访问代码(下一节提供链接),将避免复制和粘贴代码。}\par 65 | 66 | \hspace*{\fill} \par %插入空行 67 | \textbf{下载示例} 68 | 69 | 可以从GitHub网站\url{https://github.com/PacktPublishing/Learn-LLVM-12}下载本书的示例代码。如果有对代码的更新,也会在现有的GitHub存储库中更新。\par 70 | 71 | 我们还在\url{https://github.com/PacktPublishing/}上提供了丰富的图书和视频目录中的其他代码包。可以一起拿来看看!\par 72 | 73 | \hspace*{\fill} \par %插入空行 74 | \textbf{实例演示} 75 | 76 | 本书代码的演示视频可以在\url{https://bit.ly/3nllhED}上查看 \par 77 | 78 | \hspace*{\fill} \par %插入空行 79 | \textbf{下载彩图} 80 | 81 | 我们还提供了一个PDF文件,里面有本书中使用的屏幕截图/图表的彩色图像。可在此下载:\url{https://static.packt-cdn.com/downloads/9781839213502\_ColorImages.pdf}。\par 82 | 83 | \hspace*{\fill} \par %插入空行 84 | \textbf{联系方式} 85 | 86 | 我们欢迎读者的反馈。\par 87 | 88 | 反馈:如果你对这本书的任何方面有疑问,需要在你的信息的主题中提到书名,并给我们发邮件到\url{customercare@packtpub.com}。\par 89 | 90 | 勘误:尽管我们谨慎地确保内容的准确性,但错误还是会发生。如果您在本书中发现了错误,请向我们报告,我们将不胜感激。请访问\url{www.packtpub.com/support/errata},选择相应书籍,点击勘误表提交表单链接,并输入详细信息。\par 91 | 92 | 盗版:如果您在互联网上发现任何形式的非法拷贝,非常感谢您提供地址或网站名称。请通过\url{copyright@packt.com}与我们联系,并提供材料链接。\par 93 | 94 | 如果对成为书籍作者感兴趣:如果你对某主题有专长,又想写一本书或为之撰稿,请访问\url{authors.packtpub.com}。\par 95 | 96 | \hspace*{\fill} \par %插入空行 97 | \textbf{欢迎评论} 98 | 99 | 请留下评论。当您阅读并使用了本书,为什么不在购买网站上留下评论呢?其他读者可以看到您的评论,并根据您的意见来做出购买决定。我们在Packt可以了解您对我们产品的看法,作者也可以看到您对他们撰写书籍的反馈。谢谢你!\par 100 | 101 | 想要了解Packt的更多信息,请访问\url{packt.com}。\par 102 | 103 | \newpage 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Learn-LLVM-12/a532559da9a9694ef055f5b84c4f0c3f769ea601/cover.png --------------------------------------------------------------------------------