南京大学/NJU 人工智能/AI 计算机系统基础/ICS PA3 记录

异常响应机制

简单来说,这个机制就是在异常发生的时候,执行一个异常指令,碰到这个指令,操作系统就会中断应用程序,开始干活。

状态机的扩充:为了实现这个目的,会多几个异常寄存器,也就是除了通用寄存器 GPR,还会有系统寄存器 SR。

指令执行的失败的确定性:指令可能触发的异常是确定的,我们明确知道某条指令是否会触发异常,因此,只需要把异常指令同样视为一种状态转移即可。

选做题 特殊的原因? (建议二周目思考)

这些程序状态(x86的eflags, cs, eip; mips32的epc, status, cause; riscv32的mepc, mstatus, mcause)必须由硬件来保存吗? 能否通过软件来保存? 为什么?

因此在 isa-def.h 中需要添加异常寄存器的定义:

typedef struct {
  word_t gpr[MUXDEF(CONFIG_RVE, 16, 32)];
  vaddr_t pc;
  word_t mtvec, mepc, mcause;
} MUXDEF(CONFIG_RV64, riscv64_CPU_state, riscv32_CPU_state);

然后在 isa_raise_intr() 函数中,就是实现记录异常信息,并返回异常处理入口地址:

word_t isa_raise_intr(word_t NO, vaddr_t epc) {
  /* 1. 硬件响应第一步: 保存触发异常时的PC到mepc */
  cpu.mepc = epc;
  
  /* 2. 硬件响应第二步: 设置异常原因/编号到mcause */
  cpu.mcause = NO;

  /* 3. 硬件响应第三、四步: 从mtvec取出异常入口地址并返回,NEMU将跳转到此地址 */
  
  // mtvec中存放的就是 __am_asm_trap 的地址。
  // 我们只取 mtvec 的基地址部分 (清除低两位,即 Direct Mode)
  word_t mtvec_base = cpu.mtvec & ~0x3UL; 
  
  return mtvec_base;
}

选做题 x86通过软件来保存异常号, 没有类似cause的寄存器. mips32和riscv32也可以这样吗? 为什么?

可以做到,但不建议。mcause 是比较重要的内容,直接用硬件保存会很方便。

选做题 我们知道进行函数调用的时候也需要保存调用者的状态: 返回地址, 以及calling convention中需要调用者保存的寄存器. 而CTE在保存上下文的时候却要保存更多的信息. 尝试对比它们, 并思考两者保存信息不同是什么原因造成的.

函数调用是同级操作,需要双方共同遵守 ABI,各自对自己的部分负责,如果发生错误则难以挽回。CTE 保存上下文是跨级操作,上级系统需要处理下级产生的锅,下级可能会不守规则,因此上级必须要完全隔绝下级不守规则造成的损害。例如通用寄存器在函数调用中由双方共同负责,而在 CTE 保存上下文中则是操作系统需要保证他们的值正确。

etrace实现

在 isa_raise_intr 中输出信息,说明有异常发生即可。

PA3.1 终结,总结一下异常处理的流程吧~

(AM中)通过 cte_init 注册回调函数,然后使用 yield 函数自陷,在汇编代码中加入 ecall 指令;(来到NEMU)解析 ecall 指令首先调用 isa_raise_intr 获得异常入口地址并更新 CSR 寄存器的值,下一步执行异常入口地址的指令;(AM中)而这个指令在 cte_init 中被定义为 __am_asm_trap 函数的地址,这个函数的汇编写在 trap.S 文件中,文件执行到一半,调用 __am_irq_handle 函数,首先根据 mcause 为活动分配类型,然后执行最开始注册的回调函数。

用户程序和系统调用

选做题 我们提到了代码和数据都在可执行文件里面, 但却没有提到堆(heap)和栈(stack). 为什么堆和栈的内容没有放入可执行文件里面? 那程序运行时刻用到的堆和栈又是怎么来的? AM的代码是否能给你带来一些启发?

堆和栈是在进程开启时动态分配的,因为他们的内容是高度动态的,根本不可能预先定好,根据不同的输入有不同的过程,自然放不进去。

选做题 如果你在GNU/Linux下执行一个从Windows拷过来的可执行文件, 将会报告"格式错误". 思考一下, GNU/Linux是如何知道"格式错误"的?

文件头会描述一个文件的类型。

任务 实现 loader

模拟的就是把 elf 文件从 disk 加载到 ram 中,那究竟要加载什么内容呢?根据所学的知识,就是程序头表里面,某个段的 type 为 PT_LOAD 就是要加载的。

因此,我们要做的就是:

  • 读取 elf 头并验证
  • 阅读程序头表
  • 读每个段的对应 Type
  • 如果这个段需要载入,则载入到对应位置
  • 最后返回程序执行入口

“如果这个段需要载入,则载入到对应位置”这一步稍微卡住我了。静下来分析,从 ELF 文件的 offset 位置开始,复制 Filesz 长度的信息,粘贴到 vaddr 位置。

static uintptr_t loader(PCB *pcb, const char *filename) {
  uintptr_t offset = 0;

  Elf_Ehdr ehdr;
  ramdisk_read(&ehdr, offset, sizeof(Elf_Ehdr));
  assert(*((uint32_t*)(ehdr.e_ident)) == 0x464c457f);

  Elf_Phdr phdr;
  uintptr_t phdr_offset = offset + ehdr.e_phoff;
  for(int i = 0; i < ehdr.e_phnum; ++i) {
    ramdisk_read(&phdr, phdr_offset + i * ehdr.e_phentsize, ehdr.e_phentsize);
    if(phdr.p_type == PT_LOAD) {
      uintptr_t p_vaddr = phdr.p_vaddr;
      size_t filesz = phdr.p_filesz;
      size_t memsz = phdr.p_memsz;
      uintptr_t p_offset = phdr.p_offset;

      ramdisk_read((void*)p_vaddr, offset + p_offset, filesz);
      if(memsz > filesz) {
        memset((void*)(p_vaddr + filesz), 0, memsz - filesz);
      }
    }
  }
  return ehdr.e_entry;
}

运行时环境

选做题 对于批处理系统来说, 系统调用是必须的吗? 如果直接把AM的API暴露给批处理系统中的程序, 会不会有问题呢?

肯定有问题。这就是跨越权限了。

识别系统调用

在 __am_irq_handle 函数中添加 SYSCALL 的类型分配即可。

Context* __am_irq_handle(Context *c) {

  // printf("mcause: %u\n", c->mcause);
  // printf("mepc: %u\n", c->mepc);
  // printf("mstatus: %u\n", c->mstatus);

  if (user_handler) {
    Event ev = {0};
    switch (c->mcause) {
      case 8: ev.event = (int)c->GPR1 < 0 ? EVENT_YIELD : EVENT_SYSCALL; break;
      case 11: ev.event = EVENT_SYSCALL; break;
      case 13: // Load Page Fault / Load Access Fault (未处理的访存错误)
      case 15: // Store/AMO Page Fault / Store/AMO Access Fault (未处理的写回错误)
          ev.event = EVENT_PAGEFAULT;
          break;
      default: ev.event = EVENT_ERROR; break;
    }

    c = user_handler(ev, c);
    assert(c != NULL);
  }

  return c;
}

实现 SYS_yield 系统调用和 SYS_exit 系统调用

就是根据异常号处理不同的处理系统调用异常:

// syscall.c

static uintptr_t sys_yield() {
  yield();
  return 0;
}

void do_syscall(Context *c) {
  uintptr_t a[4];
  a[0] = c->GPR1;

  switch (a[0]) {
    case SYS_yield: c->GPRx = sys_yield(); break;
    case EVENT_YIELD_MAGIC: yield(); c->GPRx = 0; break;
    default: panic("Unhandled syscall ID = %d", a[0]);
  }
}

选做题 如果你选择的是RISC-V, 你会发现它并不是通过a0来传递系统调用号. 事实上, 我们参考了RISC-V Linux的系统调用参数传递的约定: 即在RISC-V Linux上也是通过这个寄存器来传递系统调用号的. 你觉得RISC-V Linux为什么没有使用a0来传递系统调用号呢?

因为 riscv32 的 abi 普通函数调用规范就是用 a0 传参,那么自然让 a0 就是异常处理函数的参数,系统调用号另用其它。

实现 strace

和 etrace 一样,输出一下信息即可

//syscall.c
void do_syscall(Context *c) {
  uintptr_t a[4];
  a[0] = c->GPR1;
  a[1] = c->GPR2;
  a[2] = c->GPR3;
  a[3] = c->GPR4;
  uintptr_t ret;

#ifdef enable_strace
  if ((intptr_t)a[0] >= 0) {
    printf("[strace] SYS_ID=%d (0x%x), Args: (0x%x, 0x%x, 0x%x)\n", a[0], a[0], a[1], a[2], a[3]);
  }
#endif

  switch (a[0]) {
    case SYS_yield: ret = sys_yield(); break;
    case EVENT_YIELD_MAGIC: yield(); ret = 0; break;
    case SYS_exit: sys_exit(a[1]); break;
    default: panic("Unhandled syscall ID = %d", a[0]);
  }

  if(a[0] != SYS_exit) {
    c->GPRx = ret;
#ifdef enable_strace
    printf("[strace] SYS_ID=%d returned: 0x%x\n", a[0], ret);
#endif
  }
}

标准输出

根据定义实现即可,记得要把 navy-apps 里面的 _write 系统调用写上正确的东西,而非 _exit。

static size_t sys_write(int fd, const void* buf, size_t count) {
  int ret = 0;
  char* data = (char*)buf;
  switch(fd) {
    case 1: case 2:
      for(int i = 0; i < count; ++i)
        putch(data[i]);
      ret = count;
      break;
  }
  return ret;
}

堆区管理

可能写的很烂的实验手册会让人疑惑究竟要写哪些代码呢?不妨来推理一下。

返回看 write 的运行逻辑:

(navy-apps)中调用write函数,会调用_write发出对应的syscall,hello程序连着这些运行时环境被做成ELF文件后,放到nanos-lite的ramdisk.img文件;(nanos-lite中)init_proc函数中调用naive_load将ramdisk.img载入内存中,并且调用这个文件;(navy-apps)接着就会碰到发出syscall的指令,通过AM和NEMU的处理后,回到操作系统;(nanos-lite中)调用 do_event 然后调用 do_syscall,最后调用我们写的 sys_write,...后面就先不关心了

弄清楚这一点后,再结合内容理解,malloc->sbrk->_sbrk->syscall->sys_brk,而特别关注 sbrk 和 sys_brk 的参数,作用和返回值就可以写出代码了:

//syscall.c

void *_sbrk(intptr_t increment) {
  if(program_break == NULL) {
    program_break = &_end;
  }
  void *old_program_break = program_break;
  void *new_program_break = program_break + increment;
  intptr_t syscall_ret = _syscall_(SYS_brk, (intptr_t)new_program_break, 0, 0);

  if (syscall_ret == 0) {
    program_break = new_program_break;
    return old_program_break;
  } else {
    return (void *)-1; 
  }
}

简易文件系统

一步步理解并实现 sfs(simplified file system)。

为了简单起见,双方遵守以下约定:

  • 每个文件的大小是固定的
  • 写文件时不允许超过原有文件的大小
  • 文件的数量是固定的, 不能创建新文件
  • 没有目录

那么第二条给了我们底气:我们约定文件从ramdisk的最开始一个挨着一个地存放。

使用 “文件记录表” 来记录磁盘中的文件信息:

typedef struct {
  char *name;         // 文件名
  size_t size;        // 文件大小
  size_t disk_offset;  // 文件在ramdisk中的偏移
} Finfo;

在 sys 中,这三项信息都是固定不变的;...;我们把目录分隔符/也认为是文件名的一部分。

“为了统一管理它们, 我们希望通过一个编号来表示文件, 这个编号就是文件描述符(file descriptor). 一个文件描述符对应一个正在打开的文件, 由操作系统来维护文件描述符到具体文件的映射.”重点就在这里 一个文件描述符对应一个正在打开的文件,每个文件有一个对应的数字。

“在Nanos-lite中, 由于sfs的文件数目是固定的, 我们可以简单地把文件记录表的下标作为相应文件的文件描述符返回给用户程序.”重点就是 文件记录表的下标是 FD。

“引入偏移量属性open_offset, 来记录目前文件操作的位置. 每次对文件读写了多少个字节”

选做题 事实上在真正的操作系统中, 把偏移量放在文件记录表中维护会导致用户程序无法实现某些功能. 但解释这个问题需要理解一些超出课程范围的知识, 我们在此就不展开叙述了. 你可以在学习操作系统课程的时候再来思考这个问题.由于Nanos-lite是一个精简版的操作系统, 上述问题暂时不会出现, 为了简化实现, 我们还是把偏移量放在文件记录表中进行维护.

这是因为 open_offset 是变化的,和静态的内容应该区分存放。

“偏移量可以通过lseek()系统调用来调整”->lseek()可以改变open_offset。

现在可以思考有哪些函数,要如何实现了:

//这些是 navy-apps 里面的操作
int open(const char *pathname, int flags, int mode);
size_t read(int fd, void *buf, size_t len);
size_t write(int fd, const void *buf, size_t len);
int close(int fd);
size_t lseek(int fd, size_t offset, int whence);
//这些是操作系统,即nanos-lite里面的操作
int fs_open(const char *pathname, int flags, int mode);
size_t fs_read(int fd, void *buf, size_t len);
size_t fs_write(int fd, const void *buf, size_t len);
size_t fs_lseek(int fd, size_t offset, int whence);
int fs_close(int fd);

按照对应的逻辑写即可:


#define NR_FILES (sizeof(file_table) / sizeof(file_table[0]))

#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2

size_t fs_lseek(int fd, size_t offset, int whence) {
  assert(fd >= 0 && fd < NR_FILES);


  Finfo *file = &file_table[fd];

#ifdef DEBUG_TAG
  printf("fs_lseek FD: %d Name: %s\n", fd, file->name);
#endif

  size_t new_offset = 0;
  if (file->size == 0 && file->disk_offset == 0) {
    return file->open_offset;
  }
  switch (whence) {
    case SEEK_SET:
      new_offset = offset;
      break;
    case SEEK_CUR:
      new_offset = file->open_offset + offset; 
      break;
    case SEEK_END:
      new_offset = file->size + offset; 
      break;
    default:
      panic("fs_lseek: Invalid whence value");
  }
  if (new_offset > file->size) {
    new_offset = file->size;
  }
  file->open_offset = new_offset;
  return new_offset;
}

size_t update_count(Finfo *file, size_t count) {
  assert(file->size >= file->open_offset);
  if(file->open_offset + count > file->size) {
    return file->size - file->open_offset;
  }
  return count;
}

size_t ramdisk_write(const void *buf, size_t offset, size_t len);

size_t fs_write(int fd, const void* buf, size_t count) {
  

  assert(fd >= 0 && fd < NR_FILES);
  Finfo *file = &file_table[fd];

#ifdef DEBUG_TAG
  printf("Writefile NAME: %s\n", file->name);
#endif

  size_t ret;
  if(file->write != NULL) {
    ret = file->write(buf, 0, count);
  } else {
    count = update_count(file, count);
    ret = ramdisk_write(buf, file->disk_offset + file->open_offset, count);
    file->open_offset += ret;
  }
  return ret;
}

size_t ramdisk_read(void *buf, size_t offset, size_t len);

size_t fs_read(int fd, void* buf, size_t count) {

#ifdef DEBUG_TAG
  if(!((fd >= 0 && fd < NR_FILES))) {
    for(int i = 0; i < NR_FILES; ++i) {
      printf("Name: %s Size: %x DiskOff: %x OpenOff: %x\n", file_table[i].name, file_table[i].size, file_table[i].disk_offset, file_table[i].open_offset);
    }
    printf("Invalid FD %x\n", fd);
  }
#endif

  assert(fd >= 0 && fd < NR_FILES);
  Finfo *file = &file_table[fd];


#ifdef DEBUG_TAG
  printf("Readfile NAME: %s\n", file->name);
#endif

  size_t ret;
  if(file->read != NULL) {
    ret = file->read(buf, 0, count);
  } else {
    count = update_count(file, count);
    ret = ramdisk_read(buf, file->disk_offset + file->open_offset, count);
    file->open_offset += ret;
  }
  return ret;
}

size_t fs_open(const char *pathname, int flags, int mode) {


#ifdef DEBUG_TAG
  printf("Open file %s\n", pathname);
#endif

  for(int i = 0; i < NR_FILES; ++i) {
    if(strcmp(pathname, file_table[i].name) == 0) {
#ifdef DEBUG_TAG
    printf("%s's FD is %d!!!\n", pathname, i);
#endif
      fs_lseek(i, 0, SEEK_SET);
      return i;
    }
  }
  assert(0);
  return -1;
}

size_t fs_close(int fd) {

#ifdef DEBUG_TAG
  printf("Enter fs_close\n");
#endif
  return 0;
}

以及

static uintptr_t loader(PCB *pcb, const char *filename) {

  int fd = fs_open(filename, 0, 0);
  if(fd == -1) panic("Invalid filename for loader: %s\n", filename);

  Elf_Ehdr ehdr;
  fs_read(fd, (void*)&ehdr, sizeof(Elf_Ehdr));
  assert(*((uint32_t*)(ehdr.e_ident)) == 0x464c457f);

  Elf_Phdr phdr[ehdr.e_phnum];
  fs_lseek(fd, ehdr.e_phoff, SEEK_SET);

  fs_read(fd, (void*)&phdr, ehdr.e_phnum * ehdr.e_phentsize);
  for(int i = 0; i < ehdr.e_phnum; ++i) {
    if(phdr[i].p_type == PT_LOAD) {
      uintptr_t p_vaddr = phdr[i].p_vaddr;
      size_t filesz = phdr[i].p_filesz;
      size_t memsz = phdr[i].p_memsz;
      uintptr_t p_offset = phdr[i].p_offset;
      
      fs_lseek(fd, p_offset, SEEK_SET);
      fs_read(fd, (void*)p_vaddr, filesz);
      if(memsz > filesz) {
        memset((void*)(p_vaddr + filesz), 0, memsz - filesz);
      }
    }
  }

  // printf("Load End!\n");

  return ehdr.e_entry;
}

调试调了两三个小时,还是经验不足导致的。

操作系统上的 IOE

手册上篇幅很长的废话——总结来说,就是把设备看成文件,但是为每个设备设计专门的输入输出方式,统一抽象形成 sys_read/sys_write 事件,不占用系统调用号,但是在读写系统事件下分支。

把串口抽象成文件

串口很简单:

//device.c

size_t serial_write(const void *buf, size_t offset, size_t len) {
  char* data = (char*)buf;
  for(int i = 0; i < len; ++i)
    putch(data[i]);
  return len;
}

实现gettimeofday

这个事件没有被抽象成一个文件,就跟实现一个系统调用一样

//syscall.c
static intptr_t sys_gettimeofday(struct timeval *tv, struct timezone *tz) {
  if(tv == NULL || tz == NULL) return (intptr_t)-1;
  AM_TIMER_UPTIME_T upt = io_read(AM_TIMER_UPTIME);
  tv->tv_sec = upt.us / 1000000;
  tv->tv_usec = upt.us % 1000000;
  return 0;
}

然后自己写一个 timer 测试:

#include <unistd.h>
#include <stdio.h>
#include <sys/time.h>


int main() {
  struct timeval a;
  struct timezone b;
  __uint64_t last = 0;
  while(1) {
    gettimeofday(&a, &b);
    __uint64_t now = a.tv_sec * 1000000ll + a.tv_usec;
    if(now - last >= 500000) {
        last = now;
        printf("Another 0.5 Seconds!\n");
    }
  }
  return 0;
}


实现NDL的时钟

直接调用接口即可,难点在于 make 的时候链接不了,自己不知道咋弄,问了程子杰,原来是因为 timer-test 是自己写的,导致 Makefile 的内容并不全面。

uint32_t NDL_GetTicks() {
  struct timeval tv;
  if(gettimeofday(&tv, NULL) != 0) {
    return 0;
  }
  uint64_t total_ms = (uint64_t)tv.tv_sec * 1000 + (uint64_t)tv.tv_usec / 1000;
  return (uint32_t)total_ms;
}
NAME = timer
SRCS = timer.c
LIBS = libndl                   //这一行要加上
include $(NAVY_HOME)/Makefile

把按键输入抽象成文件

又是一个设备实现。有了上面的经验,我们可以认识到设备实现的过程,粗略来说,无非是:Nanos-lite(OS) 调用 AM 的接口,AM 调用 NEMU(硬件) 的接口;前者的值使用文件读写的方式传递,而后者的通过双方约定相同的虚拟内存传递。

懂得这些后,实现就没什么难度了,根据 amdev.h 中的对应接口实现即可。

//device.c
size_t events_read(void *buf, size_t offset, size_t len) {
  char* data = (char*)buf;
  AM_INPUT_KEYBRD_T event = io_read(AM_INPUT_KEYBRD);
  if(event.keycode == AM_KEY_NONE) {
    return 0;
  }
  int temp_len = strlen(keyname[event.keycode]);
  if(4 + temp_len > len) {
    return 0;
  }
  if(event.keydown) {
    data[0] = 'k';
    data[1] = 'd';
    data[2] = ' ';
  } else {
    data[0] = 'k';
    data[1] = 'u';
    data[2] = ' ';
  }
  memcpy(data + 3, keyname[event.keycode], temp_len);
  data[3 + temp_len] = '\n';
  return 4 + temp_len;
}

在NDL中获取屏幕大小

Nanos-lite需要做的, 便是把显存抽象成文件. 显存本身也是一段存储空间, 它以行优先的方式存储了将要在屏幕上显示的像素。

Nanos-lite和Navy约定, 把显存抽象成文件/dev/fb(fb为frame buffer之意), 它需要支持写操作和lseek。

NDL向用户提供了两个和绘制屏幕相关的API:

// 打开一张(*w) X (*h)的画布
// 如果*w和*h均为0, 则将系统全屏幕作为画布, 并将*w和*h分别设为系统屏幕的大小
void NDL_OpenCanvas(int *w, int *h);

// 向画布`(x, y)`坐标处绘制`w*h`的矩形图像, 并将该绘制区域同步到屏幕上
// 图像像素按行优先方式存储在`pixels`中, 每个像素用32位整数以`00RRGGBB`的方式描述颜色
void NDL_DrawRect(uint32_t *pixels, int x, int y, int w, int h);

NDL_DrawRect()的功能和PA2中介绍的绘图接口是非常类似的。

但为了实现它, NDL还需要知道屏幕大小的信息. Nanos-lite和Navy约定, 屏幕大小的信息通过/proc/dispinfo文件来获得, 它需要支持读操作。

一个合法的 /proc/dispinfo文件例子如下:

WIDTH : 640
HEIGHT:480

那么 dispinfo_read 的实现就是按照对应格式输出文件,NDL_OpenCanvas 就是使用 read 查询 /proc/dispinfo 并复制 w 和 h。

//device.c
size_t dispinfo_read(void *buf, size_t offset, size_t len) {
  AM_GPU_CONFIG_T config = io_read(AM_GPU_CONFIG);
  int ret = snprintf((char*)buf, len, "WIDTH:%d\nHEIGHT:%d\n", config.width, config.height);
  return ret < len ? ret : len;
}

//NDL.c
void NDL_OpenCanvas(int *w, int *h) {
  if (getenv("NWM_APP")) {
    ...
  } else {
    int fd = open("/proc/dispinfo", 0, 0);
    if(fd < 0) {
      *h = *w = -1; return;
    }
    char buf[64];
    int nread = read(fd, buf, sizeof(buf) - 1);
    if(nread == 0) {
      *h = *w = -1; return;
    }
    close(fd);
    sscanf(buf, "WIDTH:%d\nHEIGHT:%d\n", &screen_w, &screen_h);
    if(*w == 0 && *h == 0) {
      *w = screen_w, *h = screen_h;
    }
    canvas_w = *w, canvas_h = *h;
    canvas_x_offset = (screen_w - canvas_w) / 2;
    canvas_y_offset = (screen_h - canvas_h) / 2;

    printf("Canvas Size: %d * %d\n", *w, *h);
  }
}

把VGA显存抽象成文件

实现fb_write()(在nanos-lite/src/device.c中定义), 用于把buf中的len字节写到屏幕上offset处. 你需要先从offset计算出屏幕上的坐标, 然后调用IOE来进行绘图. 另外我们约定每次绘图后总是马上将frame buffer中的内容同步到屏幕上.

在这里,我被画布和屏幕两个概念误导了。在 AM,NEMU 中,vga 设备是可以形成一个框框实现显示功能的,而屏幕配置 GPU_CONFIG 也就是这个的规格;而 NDL 中设置的画布大小,是软件调用者规定的——毕竟你总不能说,我的应用就一定要显示在一个 300*400 的框架下吧,这不现实,因此上面的两份代码就是在分别实现这两个事情。

而接下来要实现的 fb_write,就是站在屏幕的视角下,要在 offset 处,往后连续绘制 len 长度的内容。

//device.c
size_t fb_write(const void *buf, size_t offset, size_t len) {

  int screen_w = io_read(AM_GPU_CONFIG).width;
  assert(offset % BPP == 0 && len % BPP == 0);
  assert(screen_w != 0);

  size_t off = offset / BPP;
  size_t tot_off = len / BPP;

  int sy = off / screen_w;
  int sx = off % screen_w;

  int curx = sx, cury = sy;
  size_t done = 0;

  uint32_t* curp = (uint32_t*)buf;
  
  while(done < tot_off) {
    int rest = screen_w - curx;
    if(done + rest > tot_off) rest = tot_off - done;
    io_write(AM_GPU_FBDRAW, curx, cury, curp, rest, 1, 1);
    done += rest;
    curp += rest;
    cury++, curx = 0;
  }
  return len;
}

直接调用接口,按行绘制即可。

//ndl.c

void NDL_DrawRect(uint32_t *pixels, int x, int y, int w, int h) {
  int fd = open("/dev/fb", 0, 0);
  // lseek(fd, 0, SEEK_SET);
  // write(fd, (void*)pixels, w * BPP);

  for(int j = y; j < y + h; ++j) {
    int realx = canvas_x_offset + x;
    int realy = canvas_y_offset + j;
    int offset = realy * screen_w + realx;
    lseek(fd, offset * BPP, SEEK_SET);
    write(fd, (void*)(pixels + (j - y) * w), w * BPP);
  }
  
  return;
}

精彩纷呈的应用程序

选做题 比较fixedpt和float
fixedpt和float类型的数据都是32位, 它们都可以表示2^32个不同的数. 但由于表示方法不一样, fixedpt和float能表示的数集是不一样的. 思考一下, 我们用fixedpt来模拟表示float, 这其中隐含着哪些取舍?

fixedpt 表示的整数和小数部分都是固定大小的,而 float 的整数部分和小数部分有平衡,整数大小数就小,反之亦然。这部分实现没什么意思,用 ai 吧。

运行NSlider

sdl 的接口挺复杂的,查查资料然后写。不过运行效果很有意思!

image

然后是 SDL_WaitEvent,这个就是仿照 event-test 写一个调用 NDL 的即可,有意思的是这个函数是个阻塞式函数,如果没有行为就会一直持续:

//event.c
int SDL_WaitEvent(SDL_Event *event) {
  char buf[64];
  while(1) {
    if (NDL_PollEvent(buf, sizeof(buf))) {
      break;
    }
  }
  char key[32];
  if (sscanf(buf, "kd %s", key) == 1) {
    event->type = SDL_KEYDOWN;
  } else if (sscanf(buf, "ku %s", key) == 1) {
    event->type = SDL_KEYUP;
  } else {
    return 0;
  }
  for (int i = 0; i < sizeof(keyname) / sizeof(keyname[0]); i ++) {
    if (strcmp(key, keyname[i]) == 0) {
      event->key.keysym.sym = i;
      break;
    }
  }
  return 1;
}

运行Flappy Bird

先按照第一段文字的说明,本机运行了 flappy bird,然后在 sdl_image 里面按照步骤写好 IMG_LOAD 函数即可。

运行仙剑奇侠传

image

批处理系统

展示你的批处理系统

系统调用的添加很简单,nterm 也不难:

static void sh_handle_cmd(const char *cmd) {
  if (strcmp(cmd, "clear\n") == 0) {
    term->write("\033[2J\033[H", 7);
  } else if (strncmp(cmd, "echo ", 5) == 0) {
    term->write(cmd + 5, strlen(cmd + 5));
  } else {
    int len = strlen(cmd);
    if (cmd[len - 1] == '\n') {
      ((char *)cmd)[len - 1] = '\0';
    }
    execve(cmd, NULL, NULL);
    // sh_printf("sh: command not found: %s", cmd);
  }
  return;
}

添加环境变量

直接问 ai 即可

static void sh_handle_cmd(const char *cmd) {
  if (strcmp(cmd, "clear\n") == 0) {
    term->write("\033[2J\033[H", 7);
  } else if (strncmp(cmd, "echo ", 5) == 0) {
    term->write(cmd + 5, strlen(cmd + 5));
  } else {
    int len = strlen(cmd);
    if (cmd[len - 1] == '\n') {
      ((char *)cmd)[len - 1] = '\0';
    }
    execvp(cmd, NULL);
    // execve(cmd, NULL, NULL);
    // sh_printf("sh: command not found: %s", cmd);
  }
  return;
}
posted @ 2025-11-17 15:36  哼唧昂叽  阅读(128)  评论(0)    收藏  举报