听说你想写个虚拟机(五)?

大家好,我是微微笑的蜗牛,🐌。

这是虚拟机系列的第五篇文章,主要介绍 TRAP 指令,系统调用。前四篇文章可点击下方链接进行查看。

TRAP 的意思是陷阱,当程序需要请求操作系统资源时,比如读写文件,就会从用户态「陷入」内核态,根据调用号,找到相应的程序进行处理。处理完毕后,再从内核态切回用户态。

细细一想,陷阱这个词,真的很贴切,就好像掉入了预先设好的机关一样。

接下来,我将带着大家一起实现几个系统调用,比如从键盘读入单个字符 GETC、输出字符串到屏幕 PUTS、输出单个字符到屏幕 OUT 等等。

指令格式

TRAP 指令比较特殊,操作码为 1111。低 8 位为系统调用号,用于标识不同的系统调用。

这里,我们一共定义了 6 种系统调用。系统调用号的定义如下所示:

// 类型
typedef enum
{
  TRAP_GETC = 0x20, // 从键盘输入
  TRAP_OUT = 0x21,  //输出字符
  TRAP_PUTS = 0x22, // 输出字符串
  TARP_IN = 0x23,  // 打印输入提示,读取单个字符
  TRAP_PUTSP = 0x24,// 输出字符串
  TRAP_HALT = 0x25, // 退出程序
} TrapSet;

注意 TRAP_HALT 这个系统调用。在之前的文章中,我们都是简单处理,将 TRAP 指令直接视为程序退出,并没有考虑它的参数。这里终于要揭示它的真身了,程序退出其实也是一个系统调用,调用号是 0x25。

所以,从现在开始,实例代码中,将会使用它的真正定义 0xf025,来表示退出程序。

系统调用

GETC

系统调用号是 0x20,表示从键盘读取单个字符,并将字符放入 R0 中。

指令布局如下:

我们可以直接使用 c 标准库中的 getchar 来进行模拟输入。

// 等待输入一个字符,最后存入 r0
void trap_getc()
{
    // 清空输入缓冲区
  fflush(stdin);

  reg[R_R0] = (uint16_t)getchar();
}

OUT

系统调用号是 0x21,表示将 R0 中的字符打印到屏幕。

指令布局如下:

这里,可使用 putc 函数来模拟打印字符。

// 将 r0 中的字符打印出来
void trap_out()
{
  putc((char)reg[R_R0], stdout);

    // 刷新输出缓冲区
  fflush(stdout);
}

PUTS

系统调用号是 0x22,表示打印 ASCII 字符串。从 R0 寄存器取出字符串的起始地址,将字符串打印到屏幕。

每个存储单元存储 1 个字符,存储单元大小为 2 字节,也就是 1 个字符占 2 字节

指令布局如下:

我们仍然使用 putc 来打印单个字符。由于此处是个字符串,只需循环打印即可。

我们来拆解一下实现步骤:

  1. 取出 R0 中的地址。
  2. 初始化 uint16_t 类型的指针,指向该地址。由于指针是指向 uint16_t 类型,那么意味着,通过指针取出的值是 16 位;同时指针自增时,会移动 16 字节
  3. 不断循环,指针指向下一个字符,直至遇到空字符结束。

实现代码如下:

// 将 r0 寄存器中地址处的字符串打印出来。1 个字符占 2 字节。
void trap_puts()
{
    // 1. 取出 R0 中的地址。
  uint16_t address = reg[R_R0];

    // 2. 初始化指针,指向该地址
  uint16_t *c = mem + address;

    // 3. 不断循环,指针指向下一个字符,直至遇到空字符结束
  while (*c)
  {
    putc((char)*c, stdout);
    ++c;
  }

  fflush(stdout);
}

IN

系统调用号是 0x23。和 GETC 类似,只不过多了两个打印的步骤。一是打印输入提示,二是打印输入的字符。

指令布局如下:

实现代码如下:

// 提示输入一个字符,将字符打印,并放入 R0
void trap_in()
{
    // 清空输入缓冲区
  fflush(stdin);

    // 打印提示
  printf("Enter a character:");
  char c = getchar();

    // 打印输入的字符
  putc(c, stdout);
  reg[R_R0] = (uint16_t)c;
}

PUTSP

系统调用号是 0x24,与 PUTS 类似,同样是打印 ASCII 字符串。

唯一区别是:每个存储单元存储 2 个字符,即 1 个字符占 1 字节

指令布局如下:

由于 1 个字符占 1 字节,但指向 uint16_t 类型的指针每次取值会取出 2 字节的数据。因此,两个字符需分别打印。先打印低 8 位,再打印高 8 位。

注意:当字符串长度为奇数时,字符串最后一个存储单元的高 8 位是 0,代表空结束字符。因为数据是 2 字节一组。

// 将 r0 地址处的字符串出来,一个字符一字节
void trap_put_string()
{
  uint16_t *c = mem + reg[R_R0];
  while (*c)
  {
    // 低  8 位
    char char1 = (*c) & 0xff;
    putc(char1, stdout);

    // 高 8 位
    char char2 = (*c) >> 8;

        // 当为奇数时的判断处理,此时 char2 = 0
    if (char2)
    {
      putc(char2, stdout);
    }

    ++c;
  }

  fflush(stdout);
}

HALT

系统调用号是 0x25。表示程序停止,并打印一条退出消息。

指令布局如下:

它的实现最为简单:

puts("Halt");
running = 0;

实践

到这里,我们的兵器库中又多了一件好物件,当然得拿出来练练。

关于输入/输出单字符的很好写指令,但对于输出字符串来说,现在还没有写入字符串数据的方式,需要预先构造数据。不过,不用担心。下篇文章,我们会讲到 LC-3 的汇编用法,那时候就知道到如何进行数据定义了。

所以,我们先构造一些字符串放入内存,包括两种存储模式:

  • 1 字符占 1 字节。
  • 1 字符占 2 字节。

假设内存区域 [0,9] 存储单元是代码段,[10,20] 区间是数据段。

大小端

在构造数据之前,我们先简单讲下大小端的概念,因为在写入数据时需要注意字节顺序。

字节的存储方式分为大端和小端。

  • 大端:数据的高位在低地址,低位在高地址,符合人类阅读习惯。
  • 小端:数据的高位在高地址,低位在低地址,符合计算机从低位开始处理。

比如数据 0x12345678,大小端的存储顺序如下,两者刚好相反。

由于我们的机器一般都是小端字节序,与人类的阅读顺序是相反的。所以在构造数据时,要特别注意一下字节顺序问题。

在理解了大小端之后,我们接着来讲讲不同模式下数据存储的差异。

模式一

1 字符占 2 字节。

假设字符串为 "abc",长度是 4(加上末尾空结束字符)。从内存单元 10 开始存储

对于字符 'a',它的 ASCII 码是 97,转换为 16 进制为 0x61。由于一个字符占 2 字节,那么扩展一下就变为 0x0061。

在小端字节序中,高位在高地址,低位在低地址。那么对于 0x0061 来说,0x00 是高位,应该在高地址;0x61 是低位,应该放在低地址。如下图所示:

其余字符的分析类似,不再赘述。

"abc" 整个字符串在内存中的布局为(注意,字符串末尾还有一个结束符 0x0):

模式二

1 字符占 1 字节。

假设字符串为 "defgh",长度是 6。从内存单元 14 开始存储

由于一个存储单元是 2 字节,首先我们把字符串分为 2 个字符一组。分组如下:

"de", "fg", "h"
  1. 对于 "de" 来说,它由 'd' 和 'e' 两个字符组成。而数据在内存区是从低地址往高地址存储,也就是说 'd' → 0x64 在低地址,'e' → 0x65 在高地址。再根据小端的特性,可推导出 0x64 在低位,0x65 在高位。最后得到的十六进制数为 0x6564。如下图所示:

  2. "fg",分析类似,十六进制表示为 0x6766。

  3. "h",属于落单的字符,再加上末尾的空字符,正好两个字符。十六进制表示为 0x0068。

"defgh" 在内存中的布局如下:

功能

设计的功能点如下,总共十条指令:

  • 调用 GETC 等待从键盘输入字符,使用 OUT 打印。
  • 调用 IN 等待从键盘输入字符。
  • R0 清零,再赋值为 10,指向 "abc" 的起始地址。
  • 调用 PUTS 打印 "abc"。
  • R0 清零,再赋值为 14,指向 "defgh" 的起始地址。
  • 调用 PUTSP 打印 "defgh"。

指令和数据在内存中的布局如下。为了方便查看,图中将代码和数据分开展示。

完整代码可查看:https://github.com/silan-liu/virtual-machine/blob/master/mac/vm_lc_3_3.c

总结

这篇文章,我们讲述了 6 个系统调用的实现,大都跟关输入输出有关,并对这些调用进行了实践。实现都比较简单,只是在构造数据部分,需要注意字节序的问题。

下篇文章,我们将会讲 LC-3 汇编代码的编写,数据的定义,以及如何将汇编代码转换为二进制指令。到时候,就不用这么麻烦的手写指令和构造数据了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容