8 用API显示字符串(harib17h)

从实际的应用程序开发角度来说,能显示字符串的API远比只能显示单个字符的API要来的方便,因为一次显示一串字符的情况比一次只显示一个字符的情况多得多。

从其他操作系统的显示字符串的API来看,一般有两种方式:一种是显示一串字符,遇到字符编码0则结束;另一种是先指定好要显示的字符串的长度再显示。我们到底要用哪一种呢?再三考虑之后,贪心的笔者决定在“纸娃娃系统”上同时实现两种方式(笑)。

本次的console.c节选

  1. void cons_putstr0(struct CONSOLE *cons, char *s)
  2. {
  3. for (; *s != 0; s++) {
  4. cons_putchar(cons, *s, 1);
  5. }
  6. return;
  7. }
  8. void cons_putstr1(struct CONSOLE *cons, char *s, int l)
  9. {
  10. int i;
  11. for (i = 0; i < l; i++) {
  12. cons_putchar(cons, s[i], 1);
  13. }
  14. return;
  15. }

■■■■■

哦,对了,有了这个函数,就可以简化mem、dir、type这几个命令的代码,趁着还没忘记,赶紧改良一下。

本次的console.c节选

  1. void cons_runcmd(char *cmdline, struct CONSOLE *cons, int *fat, unsigned int memtotal)
  2. {
  3. if (strcmp(cmdline, "mem") == 0) {
  4. cmd_mem(cons, memtotal);
  5. } else if (strcmp(cmdline, "cls") == 0) {
  6. cmd_cls(cons);
  7. } else if (strcmp(cmdline, "dir") == 0) {
  8. cmd_dir(cons);
  9. } else if (strncmp(cmdline, "type ", 5) == 0) {
  10. cmd_type(cons, fat, cmdline);
  11. } else if (cmdline[0] != 0) {
  12. if (cmd_app(cons, fat, cmdline) == 0) {
  13. /*不是命令,不是应用程序,也不是空行*/
  14. cons_putstr0(cons, "Bad command.\n\n"); /*这里!*/
  15. }
  16. }
  17. return;
  18. }
  19. void cmd_mem(struct CONSOLE *cons, unsigned int memtotal)
  20. {
  21. struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
  22. char s[60]; /*从此开始*/
  23. sprintf(s, "total %dMB\nfree %dKB\n\n", memtotal / (1024 * 1024), memman_total(memman) / 1024);
  24. cons_putstr0(cons, s); /*到此结束*/
  25. return;
  26. }
  27. void cmd_dir(struct CONSOLE *cons)
  28. {
  29. struct FILEINFO *finfo = (struct FILEINFO *) (ADR_DISKIMG + 0x002600);
  30. int i, j;
  31. char s[30];
  32. for (i = 0; i < 224; i++) {
  33. if (finfo[i].name[0] == 0x00) {
  34. break;
  35. }
  36. if (finfo[i].name[0] != 0xe5) {
  37. if ((finfo[i].type & 0x18) == 0) {
  38. sprintf(s, "filename.ext %7d\n", finfo[i].size);
  39. for (j = 0; j < 8; j++) {
  40. s[j] = finfo[i].name[j];
  41. }
  42. s[ 9] = finfo[i].ext[0];
  43. s[10] = finfo[i].ext[1];
  44. s[11] = finfo[i].ext[2];
  45. cons_putstr0(cons, s); /*这里!*/
  46. }
  47. }
  48. }
  49. cons_newline(cons);
  50. return;
  51. }
  52. void cmd_type(struct CONSOLE *cons, int *fat, char *cmdline)
  53. {
  54. struct MEMMAN *memman = (struct MEMMAN *) MEMMAN_ADDR;
  55. struct FILEINFO *finfo = file_search(cmdline + 5, (struct FILEINFO *) (ADR_DISKIMG + 0x002600), 224);
  56. char *p;
  57. if (finfo != 0) {
  58. /*找到文件的情况*/
  59. p = (char *) memman_alloc_4k(memman, finfo->size);
  60. file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
  61. cons_putstr1(cons, p, finfo->size); /*这里!*/
  62. memman_free_4k(memman, (int) p, finfo->size);
  63. } else {
  64. /*没有找到文件的情况*/
  65. cons_putstr0(cons, "File not found.\n"); /*这里!*/
  66. }
  67. cons_newline(cons);
  68. return;
  69. }

代码缩减了12行,什么嘛!一开始就这样写不就好了吗?不过不管怎么说也算是个值得高兴的事吧。

在上面字符串中我们使用了“\n”这个新的符号,这里来讲解一下。在C语言中,“\”这个字符有特殊的含义,用来表示一些特殊字符。这里出现的“\n”代表换行符,即0x0a,也就是说用2个字符来表示1个字节的信息,有点怪吧。此外还有“\t”,它代表制表符,即0x09。顺便说一下,换行符“\n”之所以用“n”,是因为它是“new line”的缩写。

■■■■■

好,我们已经有了cons_putstr0和cons_putstr1,那么怎样把它们变成API呢?最简单的方法就是像显示单个字符的API那样,分配INT 0x41和INT 0x42来调用这两个函数。不过这样一来,只能设置256个项目的IDT很快就会被用光。

既然如此,我们就借鉴BIOS的调用方式,在寄存器中存入功能号,使得只用1个INT就可以选择调用不同的函数。在BIOS中,用来存放功能号的寄存器一般是AH,我们也可以照搬,但这样最多只能设置256个API函数。而如果我们改用EDX来存放功能号,就可以设置多达42亿个API函数,这样总不会不够用了吧。

功能号暂时按下面那样划分,寄存器的用法也是随意设定的,如果不喜欢的话尽管修改就好哦。

功能号1……显示单个字符(AL = 字符编码)

功能号2……显示字符串0(EBX = 字符串地址)

功能号3……显示字符串1(EBX = 字符串地址,ECX = 字符串长度)

接下来我们将_asm_cons_putchar改写成一个新的函数。

本次的naskfunc.nas节选

  1. _asm_hrb_api:
  2. STI
  3. PUSHAD ; 用于保存寄存器值的PUSH
  4. PUSHAD ; 用于向hrb_api传值的PUSH
  5. CALL _hrb_api
  6. ADD ESP,32
  7. POPAD
  8. IRETD

这个函数非常短,因为我们想尽量用C语言来编写API处理程序,而且这样各位读者也更容易理解。

用C语言编写的API处理程序如下。

本次的console.c节选

  1. void hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
  2. {
  3. struct CONSOLE *cons = (struct CONSOLE *) *((int *) 0x0fec);
  4. if (edx == 1) {
  5. cons_putchar(cons, eax & 0xff, 1);
  6. } else if (edx == 2) {
  7. cons_putstr0(cons, (char *) ebx);
  8. } else if (edx == 3) {
  9. cons_putstr1(cons, (char *) ebx, ecx);
  10. }
  11. return;
  12. }

嗯,还是挺好理解的吧。开头的寄存器顺序是按照PUSHAD的顺序写的,如果在_asm_hrb_api中不用PUSHAD,而是一个一个分别去PUSH的话,那当然可以按照自己喜欢的顺序来。

啊,对了对了,我们还得改一下IDT的设置,将INT 0x40改为调用_asm_hrb_api。

  1. void init_gdtidt(void)
  2. {
  3. (中略)
  4. /* IDT设定*/
  5. set_gatedesc(idt + 0x20, (int) asm_inthandler20, 2 * 8, AR_INTGATE32);
  6. set_gatedesc(idt + 0x21, (int) asm_inthandler21, 2 * 8, AR_INTGATE32);
  7. set_gatedesc(idt + 0x2c, (int) asm_inthandler2c, 2 * 8, AR_INTGATE32);
  8. set_gatedesc(idt + 0x40, (int) asm_hrb_api, 2 * 8, AR_INTGATE32); /*这里!*/
  9. return;
  10. }

■■■■■

这样改写之后,现在的hello.nas就无法正常运行了,因为我们需要往EDX里面存入1才能调用相应的API。虽说我们加上一条向EDX中存入1的指令就可以,不过既然已经写好了cons_putstr0,那就干脆用这个新的API写一个hello2.nas吧。

本次的hello2.nas

  1. [INSTRSET "i486p"]
  2. [BITS 32]
  3. MOV EDX,2
  4. MOV EBX,msg
  5. INT 0x40
  6. RETF
  7. msg:
  8. DB "hello",0

哇,这样貌似短多了,make了一下,只有19个字节,创造了最小文件纪录哦。

好,完工了,赶紧“make run”,运行“hello2”试试看。

8 用API显示字符串(harib17h) - 图1

咦?什么都没显示出来?

呃……貌似失败了,怎么回事呢……?今天已经很累了,脑子都不转了,我们还是明天再来找原因吧。总之,我们先将这个放在一边,在以前的hello.nas中加一条EDX = 1;试试看吧。

本次的hello.nas

  1. [INSTRSET "i486p"]
  2. [BITS 32]
  3. MOV ECX,msg
  4. MOV EDX,1 ; 这里!
  5. putloop:
  6. MOV AL,[CS:ECX]
  7. CMP AL,0
  8. JE fin
  9. INT 0x40
  10. ADD ECX,1
  11. JMP putloop
  12. fin:
  13. RETF
  14. msg:
  15. DB "hello",0

看看这次怎么样。“make run”试验一下。

8 用API显示字符串(harib17h) - 图2

嗯嗯,这次貌似成功了

成功了,总算稍稍松了口气。

今天我们在最后的最后碰了个大钉子(就是hello2),心情有点不爽,不过已经困得不行了,就先到这吧!大家明天见。