从零开始的编译原理(2):编译程序架构

一、前言 本质是存在的真理,是自己过去了的或内在的存在。 —— 格奥尔格·威廉·弗里德里希·黑格尔《小逻辑》第二篇 本质论 §112 本篇文章是一个间章。旨在衔接文法理论与编译过程,从架构上概述整个编译程序。文法理论旨在解释自然语言,而编译过程却要创造新的语言。编译程序通过将文法规则机械化,创造出易于理解的高级语言,实现了计算的高级抽象。 二、何为 “编译” 为了理解何为 “编译”,我们可以从一个具体的编译器开始。这里我们以 C 语言的编译过程为例: 现在有一个简单的由 C 语言文法写成的文本文件 test.c // test.c #include <stdio.h> int main() { printf("hello, world.\n"); return 0; } 想要将其编译为可执行文件,当然可以使用 gcc test.c -o test 或 clang test.c -o test,但是这样就不能反映编译的具体过程了。所以这里我编写了一个 Makefile,用来指明 gcc 编译时所经历的具体步骤。 # Makefile srcname = test cc = gcc # Default target all: $(srcname) @echo "Finish!" # Linking stage $(srcname): $(srcname).o @echo "Linking stage: Creating executable '$(srcname)'" $(cc) $(srcname).o -o $(srcname) # Assembly stage $(srcname)....

一月 6, 2024 · 3 分钟 · 442 字 · Wokron

从零开始的编译原理(1):文法理论

一、前言 一个人进行推理时…… 这种过程如果是用语词进行的,他便是在心中把各部分的名词序列连成一个整体的名词或从整体及一个部分的名词求得另一个部分的名词。 —— 托马斯·霍布斯《利维坦》第一部分 论人类 第五章 论推理与学术 当霍布斯将他的机械唯物主义运用于人的思想时,语言便成为了人脑的齿轮。人的精神当然并非机械,可这种认识却也敏锐地察觉到了语言的一些本质,那就是语言是一些规则,通过这些规则的拼凑,我们得以表述我们的思想。如今,这种规则我们称之为文法。 语言学家的一部分工作是从现有的自然语言中总结文法,让语言成为机械。但是这并不是我们的重点,我们所要探索的是一个更加年轻的领域,这个领域的研究从机械出发,却要去创造千变万化的语言,让机械成为语言。 二、自然语言的文法:一个例子 但是要开始我们的旅程,还需要从自然语言开始。当我们听别人说话,或者阅读一段句子的时候,我们究竟在理解什么?比如说,现在有这样一句话,你知道我的叉子被放在哪里吗。让我们尝试理解这个句子。 这部分默认各位已有了对词性的基本认识,至少知道名词、形容词、动词、副词等等的大致含义。否则的话,这部分内容实难描述。 另外注意对于自然语言的相关表述可能并不完全符合人的认知的真实情况,这一部分只是为了方便理解。 首先需要明确一点,抛开我们所有的经验来看,语言就是符号的序列。也就是说语言是在时间上存在先后顺序的许多符号。这意味着我们的理解过程必定是从前到后的。对于上述句子来说,我们先读到 你,之后才能读到 知,再之后 道、我 等等。 另外,在这样不断读到一个个符号的时候,我们也并非单纯的按照字母或音节分别认识,而是首先明确哪些音节或符号共同组成相同含义,并将其作为一个单词来识别。在自然语言处理领域,这称为“分词”;而对于编译来说,这称为“词法分析”。 我们理解这个句子的过程可能是这样的: 读到 你,这指代一个事物,为名词 读到 知道,这指代一个行为,为动词 读到 我的,这描述了事物的属性,为形容词 读到 叉子,这指代了另一个事物,为名词 形容词 我的 和名词 叉子 组成 我的叉子,也是个名词 读到 被放,这是动词。 读到 在,这表达了一个关系,为介词 读到 哪里,这是名词 介词 在 和 名词 哪里 组成 在哪里,用于描述行为,为副词 动词 被放 和副词 在哪里 组成 被放在哪里,也是个动词。 名词 我的叉子 和动词 被放在哪里 组成 我的叉子被放在哪里,用于陈述一个事实,称为陈述句 名词 你、动词 知道 和句子 我的叉子被放在哪里 组成了 你知道我的叉子被放在哪里,是另一个陈述句。 读到 吗,这是一个疑问语气词,用于表达疑问。 句子 你知道我的叉子被放在哪里 和疑问语气词 吗 组成 你知道我的叉子被放在哪里吗,表达对被陈述的事情真实性的询问,是疑问句。 读完了所有的单词,最后得到的疑问句就是整个句子。 在上述过程中,我们不断将小的句子成分归纳为大的句子成分,在这样归纳的过程中,我们将句子中的每个单词相互关联起来,最终理解句子表达的含义。从中我们可以知道这是一个疑问,想要得到的回答是“你”与“我的叉子被放”两个事物间是否存在“知道”的关系等等。(这样说话好别扭。)...

一月 4, 2024 · 8 分钟 · 1629 字 · Wokron

KeepAlive

最近这段时间文章似乎写得不太多,但是这个小小博客并非就此死掉了。为了一些可能并不存在的关注者,同时也为了我自己,先在这里发一篇短文。 现在的我处于这样的一个阶段,不再满足于罗列刚刚学到的知识,希望在文章中加上自己的思考;但同时自己却也没有经年的储备,稍有新意的认识只能慢慢积累。这就导致了一部分内容不必写;而另一部分内容又还不能写。有时我也想稍稍水一些字数,把那些不必写的内容写一写,可近期的学业又没有留给我那么多可浪费的时间。对这一点,我要向我的博客道歉。 可道歉归道歉,我并没有忘记写文章这件事。自我上一篇文章完成已有一个半月的时间,在这期间我也构思了许久自己要做些什么。就在今天,我从考试中解放出来,接下来的计划也早就在我脑中成熟了。 操作系统实验的笔记是本博客阅读量较多的一系列文章,接下来我希望以类似的形式写一系列编译技术的文章。从编译的理论知识出发,辅以相应代码,逐渐编写一个简单的编译器。这也是我对自己的编译技术课程的总结。 说出去的话就不能反悔了。本文也是对我自己的鞭策。

一月 3, 2024 · 1 分钟 · 5 字 · Wokron

了解 LLVM IR,从手写开始

一、总览 如果你想要了解 LLVM,那么有几种可能呢?你或许是想要创造一门新的编程语言的技术爱好者;或是陷入编译课的泥沼不能自拔的大学生,但不管怎么样,你都只希望使用 LLVM 做一件事:生成目标代码。 但是这一过程并不容易。你大概率经过了编译前端的艰辛历程,但是现在在你面前的是另一座大山,LLVM IR。在最开始就看到什么基本块、虚拟寄存器、phi 节点之类的概念使你心烦意乱;各种各样的代码示例中充满了和重点并不相关的细节。这些都使你无从下手…… 或许一个更好的学习方式是将 LLVM IR 视为一种特殊的编程语言,而非编译器的 “中间表示”。这种语言站在了高级与低级语言之间,既具备了一定的抽象能力,又反映了底层汇编的工作原理。 本篇文章,就将以编程语言的视角粗略介绍 LLVM IR。通过直接手写 LLVM IR,逐步分析其特点和原理。希望能够有所帮助。 (1)准备工作 LLVM IR 虽然是中间代码,但是 LLVM 也提供了 lli 工具用于解释/即时编译执行 LLVM IR 文件。这样 LLVM IR 又与 Java 字节码有了一些相似之处。使用 lli 可以直接运行我们手写的中间代码,很是方便。使用 lli 这需要安装 LLVM。 sudo apt-get install llvm 另外,LLVM 中是没有内建输入输出的,因为 LLVM 的工作在操作系统之下。这就导致了一个问题,我们不能得知程序的执行结果。一种方法是可以用函数的返回值来输出,但是这样输出只能是单个数值。为了方便起见,可以先用 c 语言编写一个输出库。并使用 clang 将其编译成 LLVM IR。为此需要安装 clang sudo apt-get install clang 对于输出库,这里只简单定义 putint 函数。 // lib.c #include <stdio.h> void putint(int i) { printf("%d\n", i); } 使用 clang 将其翻译成中间代码。不要担心,我们并不需要关心生成的 lib....

十一月 3, 2023 · 17 分钟 · 3612 字 · Wokron

不务正业:玩个 zsh

一、前言 不知道为什么,这学期看到好几个人都用 zsh。最近恰好有时间,我也是时候换一个 shell 了。 不过嘛,shell 毕竟是用来工作的,不是一件艺术品,所以这里的配置也是以实用为主了。本文也没有用到 oh-my-zsh,用一个专门的框架来管理 shell 的配置在我看来还是太过沉重了。 本篇文章所用的环境当然是 linux。更加具体来说是 debian 系的发行版。 二、zsh 的安装 这里当然不会仔细介绍 zsh,将网上许多其他文章都重复过的内容再重复一遍。zsh 只是一个 shell,只不过这个 shell 有较强的可扩展性,同时具有一些比较有用的特性,仅此而已。 安装 zsh,只需要一条命令 apt-get install zsh 之后可以查看 /etc/shells 文件的内容,该文件记录了系统中已有的所有 shell,此时应当也有 zsh。 cat /etc/shells 本人的输出结果是这样的 # /etc/shells: valid login shells /bin/sh /bin/bash /usr/bin/bash /bin/rbash /usr/bin/rbash /usr/bin/sh /bin/dash /usr/bin/dash /bin/zsh /usr/bin/zsh 之后首次运行 zsh zsh 应该会出现如下内容,因为此时我们并未创建 zsh 的配置文件。正如 bash 的 .bashrc。这里只需要选择 0 即可。之后会在用户目录创建一个空的 .zshrc,该文件将在下一小节中详细说明。选择后重新输入命令 zsh 进入 zsh,此时应该可以看到输入提示符等内容。 This is the Z Shell configuration function for new users, zsh-newuser-install....

十月 21, 2023 · 4 分钟 · 663 字 · Wokron

面向对象的 C 语言

一、前言——对象与过程 碎碎念:这篇文章里提到的语言是真的多:c、c++、c#、java、python、golang c 语言怎么能面向对象呢?c 语言的设计当然并非为面向对象做出考虑,但是其拥有的语法却足以使我们写出具有面向对象味道的代码了。因为无论是面向过程或面向对象,其背后的本质思想都是相同的,那就是这样一个著名的公式: $$ 程序 = 数据结构 + 算法 $$ 面向过程无非是强调其算法的一面;面向对象无非是强调其数据结构的一面。当我们使用面向过程的思想编写代码时,我们所想的是数据是函数中的参数和变量,数据在过程中流动和变化。而在面向对象中情况则反了过来:方法成为了类的成员,被类型所划分,并从属于一定数据的集合。 了解了这一点之后,再看程序语言从面向过程到面向对象的发展过程也能有新的认识。这一发展背后实际上是程序的关注点由机器向人的转变。在面向过程的时代,人们所关注的是如何操纵数据。那时的机器还没有蒙在名为抽象的面纱之下,呈现在操作者面前的依旧是赤裸裸的整个内存空间,数据与数据之间没有清晰的边界,是操作者自己组织起整个系统,为各个空间划分边界,定下名称。而在这一构筑起来的系统之上,数据本身就没有那么重要了,因为更底层已经为其提供了随时取用的接口。这时,管理流程成了另一个关键问题。因为在底层的支持下构建起来的日益庞大的应用,其自身的结构却往往不能支持其质量。于是人们以数据为界,将面条一般的数据流切割成彼此独立却又相互关联的部分。这样对象才得以诞生。 二、c语言的面向对象何以可能 说回 c 语言,当其以结构体的方式组织起数据的时候,就已经有了对象的雏形了。如果我们将函数视为所属于其第一个参数类型的方法,那么对象的方法也可以表示。但是只有这两点并非真正的面向对象,因为面向对象的三大特征——封装、继承和多态,其中的后两者还未实现。 让我们来详细分析一下继承和多态到底在表明什么。继承是两个类型间的关系,类型 A 继承了类型 B,则类型 A 具有类型 B 所具有的一切属性和方法,这意味着对于 A 和 B 这两个不同的类型,都具有所属于 B 的部分。从这一点来说,两者是相同的(也因为这种相同,子类才能不加转换的赋给父类变量)。而多态(在这里指方法的重写而不包括重载)则指子类 A 对从 B 所继承的方法的重写,使得虽是相同方法,其表现却能有所不同。 明确了继承和多态,接下来我们从数据的角度分析 c 语言为何可以面向对象。所谓的一个对象,即在地址空间中的一段连续区域。此时继承中所谓的相同,即对两个不同类型的对象,其内存空间中相同位置所表达的含义相同。如果对于 B 类型来说,偏移 4 个字节之后的 4 个字节表示一个 int 字段,那么对于继承 B 类型的 A 类型来说,偏移 4 个字节之后的 4 个字节应同样表示一个 int 字段。类似的,多态中所谓的不同,可以表达为类型中相同的方法名对应的具体过程不同。由于过程在机器码中表现为地址,那么本质上来说,多态的这种不同不过是指相同字段中的值不同罢了。 此时 c 语言中实现继承和多态的方法呼之欲出,那就是使用指针。地址指示了变量所处空间的起始位置,却不表明按何种方式解释这块区域,而指针完成了这份工作。对于所有赋给指针的地址值,其都如实翻译其中的数据,那么如果想要子类与父类按照同样的方式进行翻译,就需要子类在组织其结构时保持和父类一致。而对于多态来说,事情则更简单了,函数指针同样是指针,只要使其指向不同的函数即可。 这样也可以理解为什么 c++ 中只能使用指针实现多态(引用本质还是指针)。 Child c; Father *f = &c; // 正确 Father f2 = c; // 错误 而 c++ 中使用 new 关键字申请内存这一点也被 java、c# 等面向对象语言学了过去。java、c# 等中的类变量,实际上也和指针或引用没有区别...

十月 1, 2023 · 5 分钟 · 962 字 · Wokron