12-字符串 - 你想构建一个语言虚拟机吗?
字符串是什么?
这可能会让你感到震惊,但它们比看上去要复杂一些。由于计算机只关心字节,它没有字母 s
、!
或任何其他字母的概念。这些对我们人类来说才有意义。但我们希望用户能够在不全部使用十六进制的情况下输入和读取输出。解决方案是使用某种字符编码。这将一个特定的字符映射到一个数字上。
你可能会听到两种常见的编码:ASCII 和 UTF。我不会深入它们的历史;关于 ASCII,可以查看这篇文章,关于 UTF-8,可以查看这篇文章。我会覆盖足够的内容,以便我们能够支持字符串。
ASCII
这是一种较老的编码 (encoding
),可以表示 256 个不同的字符。大多数情况下,我们使用前 128 个。这个页面有一些好的信息。
UTF-8
由于 ASCII 字符数量有限,表示日本汉字、克林贡语和其他不使用拉丁字母的语言是困难的。UTF-8 字符可以使用 1 到 4 个字节。
应该使用哪一个……
UTF-8! ASCII 将会存在很长时间,但新技术应该使用 UTF 来处理他们的字符串。
存储的字符串看起来像什么
现在我们有了堆,我们可以开始在那里存储字符串了。目前,将我们的堆视为一个可以无限增长的数组。如果我们有一个空堆,开始时可能看起来像这样:
这个堆可以容纳 12 个字节的数据。假设我们想在我们的堆中存储字符串 Hi
。在 UTF 中,H
是数字 72
。i
是数字 60
。如果我们将它们存储在我们的堆中,它将看起来像:
[72, 60, 0, 0, 0, 0, 0, 0]
记得我们说过 UTF-8 是一个 可变 宽度系统,可以占用 1-4 个字节吗?我们怎么知道字母 H
和 i
占用 1 个字节?这取决于引导字节:
0xxx xxxx 一个单字节的 US-ASCII 代码(前 127 个字符)
(A single-byte US-ASCII code (from the first 127 characters))
110x xxxx 后续一个字节 (One more byte follows)
1110 xxxx 后续两个字节 (Two more bytes follow)
1111 0xxx 后续三个字节 (Three more bytes follow)
一个更好的问题是,我们怎么知道字符串什么时候结束?我们的字符串将以空字符终止。这意味着当我们开始读取字符串时,我们消耗字节直到我们遇到一个 0
。在 UTF-8 中,这并不用于任何其他字符,所以我们可以用它来表示字符串何时结束。
字符串常量
如果我们在编译时知道一个字符串将是什么,我们可以直接将其放入汇编代码中。处理用户输入则更棘手,这是我们稍后将处理的事情。恐怕我得先介绍一些新概念。
汇编部分(或段)
到目前为止,我们一直在自上而下地编写汇编,将任何指令放在任何地方。真正的汇编程序有多个 部分:text
部分 和 data
部分。
注 | 汇编 部分 (section ) 有时被称为 段 (segment )。 |
数据部分
这是我们存储常量的地方。
文本部分
这一部分有时也被称为 代码 部分。它包含实际的指令。
ELF
在计算机中,可执行和可链接格式(ELF (
Extensible Linking Format
),以前称为可扩展链接格式)是一种常见的标准文件格式,用于可执行文件、目标代码、共享库和核心转储 (core dumps
)。
— 维基百科
我们需要我们自己的 ELF,可以这么说。也就是说,我们需要定义我们的汇编器输出的字节码将遵循的格式。
注 | 我们可能可以使用 ELF 格式,但我在最初编写虚拟机的这部分时没有想到这一点。 |
名称
现在,我将把这个格式称为……嗯……PIE 格式。请随意提出其他名称。=)
头部 (Header)
我们这部分将模仿 ELF。ELF 保留 (reserves
) 了前 64 个字节,我们也将沿袭这一做法,用于存放我们的 PIE(位置无关可执行文件)头部。
注 | 我对 派 (pie) 情有独钟 |
我们的头部将按照以下结构编排:
- 前 4 个字节将作为魔术数字,用以标识这是一个 PIE 头部
- 第 5 个字节将用于指示 PIE 格式的版本号(目前暂定为 1)
目前,头部中我们将仅编码这些信息,未来可能会根据需要增加更多字节。那么,我们的魔术数字该如何设定呢?我们决定采用:[45, 50, 49, 45]
,这代表 ASCII 码中的 EPIE
,以十六进制形式呈现。
数据部分
数据部分将从第 65 个字节开始。这是像字符串常量这样的东西将被存储的地方。
代码部分
所有的 指令(Instructions)
都将在这里。
区分部分 (Distinguishing
)
我们怎么知道数据部分从哪个字节开始?或者代码部分?很简单!我们在 ELF 头后编码代码部分的起始字节。
假设这是头,我们程序的前 64 个字节。 (我用 … 替换了写一堆 0)
[45, 50, 49, 45, 1, 0 ... 0]
接下来的八个字节将包含代码部分开始的字节。例如:
[0, 0, 0, 0, 0, 0, 0, 200]
我们现在知道以下内容:
- 字节 0-63 是 ELF 头
- 字节 64-71 包含代码开始部分 ()
- 字节 72-199 包含 数据 部分
- 字节 200-结束包含 代码 部分
汇编指令 (Assembler Directives
)
我将介绍的下一个新概念是 指令 (directives
)。这些是对汇编器的指令,要求它做某事。在 MIPS 汇编中创建字符串常量的方法如下:
my_string: .asciiz "Hello world"
暂时忽略 my_string:
部分,我们接下来会讨论它。指令是 .asciiz
,在 MIPS 中,这意味着创建一个以空字符终止的字符串。.ascii
创建一个 不 以空字符终止的字符串。在 Intel x86 汇编中有更多的指令。
如果你好奇,可以查看这些链接:
重要的是要注意 部分 是使用指令声明的。这意味着我们的程序将开始变得更加复杂,看起来像这样:
.data
<constants here> #<常数在这里>
.code
<instructions here> #<指令在这里>
汇编标签
现在,学习最后一件新事物!汇编 标签 (Labels
)!这些让你可以标记一个常数或指令,并在代码的其他位置通过该标签引用它。在我们的语言中,我们将定义一个标签为:
-
一系列字母数字 (
alphanumeric
) 字符 -
必须在行的开始
-
由
:
结束 -
可以在
code
部分通过在前面加上@
来引用标签
在我们实现这些之后,一个将工作的小型示例程序可能看起来像:
.data
my_str: .asciiz "Hello everyone"
.code
prt @my_str
注 | 是的,我们还将编码一个新指令,PRT |
结束
许多新概念,所以我将在这里结束。在下一部分中,我们将实现标签!
原文链接及作者信息
- 原文链接:Strings - So You Want to Build a Language VM
- 作者名称:Fletcher
成语及典故
- 成语 & 典故:
A Faustian Bargain
:浮士德式的交易 (A Faustian Bargain)
,这个概念源于德国关于浮士德的传说,浮士德为了追求知识和权力,与魔鬼签订了契约,以自己的灵魂作为交换。此外,这个词也可以用来形容人们为了获取某项服务或产品,可能需要牺牲一定的隐私权或其他权利的情形。
专有名词及注释
- 字符串(String):由字符组成的数据序列,用于表示文本。
- ASCII (American Standard Code for Information Interchange): 一种基于英文字符的编码系统,可以表示 128 个不同的字符。
- UTF-8 (Unicode Transformation Format - 8-bit): 一种用于编码 Unicode 字符的可变长度字符编码,使用一到四个字节表示一个字符。
- ELF (Executable and Linkable Format): 一种用于定义程序二进制文件的标准文件格式。
- PIE (Position Independent Executable): 一种编译技术,允许程序在内存中的任何位置执行,而不需要重定位。
- 汇编语言(Assembly Language):一种低级编程语言,用于编写机器语言指令,通常用于硬件级编程。
- 指令(Directive):汇编语言中用于控制汇编器行为的特殊指令。
- 标签(Label):在汇编语言中用于标记特定位置的标识符。