5 用C语言显示字符串(2)(harib19e)

为什么字符串显示API会失败呢?怎么想都不应该是a_nask.nas的问题,难道这次又是内存段的问题吗?于是我们对操作系统进行一点修改,使其在字符串显示API被调用的时候,显示EBX寄存器的值。

临时修改过的console.c节选

  1. int *hrb_api(int edi, int esi, int ebp, int esp, int ebx, int edx, int ecx, int eax)
  2. {
  3. (中略)
  4. char s[12]; /*这里!*/
  5. if (edx == 1) {
  6. cons_putchar(cons, eax & 0xff, 1);
  7. } else if (edx == 2) {
  8. /* cons_putstr0(cons, (char *) ebx + cs_base); */ /*从此开始*/
  9. sprintf(s, "%08X\n", ebx);
  10. cons_putstr0(cons, s); /*到此结束*/
  11. } else if (edx == 3) {
  12. cons_putstr1(cons, (char *) ebx + cs_base, ecx);
  13. } else if (edx == 4) {
  14. return &(task->tss.esp0);
  15. }
  16. return 0;
  17. }

将这个版本“make run”一下,然后运行hello4.hrb,屏幕上显示出00000400。这到底是怎么回事呢?hello4.hrb的文件大小只有114个字节,这样根本不可能显示出“hello, world”嘛。

■■■■■

为什么EBX里面会被写入这样一个匪夷所思的值呢?其实是因为连接了.obj文件的bim2hrb认为“hello, world”这个字符串就应该存放在0x400这个地址中。

由bim2hrb生成的.hrb文件其实是由两个部分构成的。

  • 代码部分

  • 数据部分

虽然有两个部分,不过之前我们一直都是不考虑数据部分的。当程序中没有使用字符串和外部变量(即在函数外面所定义的变量)时,就会生成不包含数据部分的.hrb文件,因此之前的程序都没有任何问题。

.hrb文件的数据部分会在应用程序启动时被传送到应用程序用的数据段中,而.hrb文件中数据部分的位置则存放在代码部分的开头一块区域中。现在是时候了,我们来详细讲解一下.hrb文件的结构吧。

由bim2hrb生成的.hrb文件,开头的36个字节不是程序,而是存放了下列这些信息(话说,bim2hrb就是笔者做出来的,如果对这个内容有意见的话,不用找英特尔或者微软,直接找笔者投诉就好了哦……笑)。

0x0000 (DWORD) ……请求操作系统为应用程序准备的数据段的大小

0x0004 (DWORD) ……“Hari”(.hrb文件的标记)

0x0008 (DWORD) ……数据段内预备空间的大小

0x000c (DWORD) ……ESP初始值&数据部分传送目的地址

0x0010 (DWORD) ……hrb文件内数据部分的大小

0x0014 (DWORD) ……hrb文件内数据部分从哪里开始

0x0018 (DWORD) ……0xe9000000

0x001c (DWORD) ……应用程序运行入口地址 - 0x20

0x0020 (DWORD) ……malloc空间的起始地址

我们来从上到下逐一讲解吧。

■■■■■

0x0000中存放的是数据段的大小。现在在“纸娃娃系统”中,应用程序用的数据段大小固定为64KB,但根据应用程序的内容,可能会需要更多的内存空间。那么把数据段都改成1MB不就好了吗?但这样一来,明明不需要那么多内存就可以运行的程序,也会被分配很大的内存空间,内存很快就会不够用了。因此,我们就在应用程序中先写好需要多大的内存空间。

0x0004中存放的是“Hari”这4个字节。这几个字符本来没什么用,只是操作系统用来判断这是不是一个应用程序文件的标记,在文件中写入这样的标记,说不定在某些情况下就会派上用场。也许在这个世界上,除了我们的“纸娃娃系统”以外,还会有其他的软件也使用.hrb这个扩展名,那样的话,光凭扩展名来判断文件的格式就有点危险了。因此,我们在文件中加上一个标记,并在操作系统中添加相应的判断功能,如果没有找到这个标记,则停止运行该文件。

如果我们不去确认“Hari”这个标记,而错误地运行了一个数据文件的话,这就和去运行一个JPEG文件差不多,会造成很严重的后果。不过现在我们使用了异常处理功能来保护操作系统,像磁盘数据被清除以及损坏电脑这种情况,已经完全可以避免了,而且操作系统也不会发生宕机。能做到这些,都是异常处理的功劳。

0x0008中存放的内容为“数据段内预备空间的大小”,不过这个值目前还没什么用(说不定以后也不会有什么用),大家不用管它就是了。在hello4.hrb中,这个值并没有被设置,所以为0。

0x000c中存放的是应用程序启动时ESP寄存器的初始值,也就是说在这个地址之前的部分会被作为栈来使用,而这个地址将被用于存放字符串等数据。在hello4.hrb中,这个值为0x400……也就是说ESP寄存器的初始值为0x400,并且分配了1KB的栈空间。1KB这个数是从哪里来的呢?其实是在生成hello4.bim的时候,在Makefile中设置的(注意看“stack:1k”这里!)。

  1. hello4.bim : hello4.obj a_nask.obj Makefile
  2. $(OBJ2BIM) @$(RULEFILE) out:hello4.bim stack:1k map:hello4.map \
  3. hello4.obj a_nask.obj

■■■■■

0x0010中存放的是需要向数据段传送的部分的字节数。

0x0014中存放的是需要向数据段传送的部分在.hrb文件中的起始地址。

0x0018中存放的是0xe9000000这个数值,这个数在内存中存放的时候形式为“00 00 00 E9”。前面几个00的部分没什么用,后面的E9才是关键。其实E9是JMP指令的机器语言编码,和后面4个字节合起来的话,就表示JMP到应用程序运行的入口地址。

0x001c中存放的是应用程序运行入口地址减去0x20后的值。为什么不直接写上入口地址而是要减掉一个数呢?因为我们在0x0018(其实是0x001b)写了一个JMP指令,这样可以通过JMP指令跳转到应用程序的运行入口地址。通过这样的处理,只要先JMP到0x001b这个地址,程序就可以开始运行了。

0x0020中存放的是将来编写应用程序用malloc函数时要使用的地址,因此现在先不用管它。malloc这个函数和memman_alloc函数十分相似。

■■■■■

根据上面的讲解,我们来修改console.c。

本次的console.c节选

  1. int cmd_app(struct CONSOLE *cons, int *fat, char *cmdline)
  2. {
  3. int segsiz, datsiz, esp, dathrb;
  4. (中略)
  5. if (finfo != 0) {
  6. /*找到文件的情况*/
  7. p = (char *) memman_alloc_4k(memman, finfo->size);
  8. file_loadfile(finfo->clustno, finfo->size, p, fat, (char *) (ADR_DISKIMG + 0x003e00));
  9. if (finfo->size >= 36 && strncmp(p + 4, "Hari", 4) == 0 && *p == 0x00) {
  10. segsiz = *((int *) (p + 0x0000));
  11. esp = *((int *) (p + 0x000c));
  12. datsiz = *((int *) (p + 0x0010));
  13. dathrb = *((int *) (p + 0x0014));
  14. q = (char *) memman_alloc_4k(memman, segsiz);
  15. *((int *) 0xfe8) = (int) q;
  16. set_segmdesc(gdt + 1003, finfo->size - 1, (int) p, AR_CODE32_ER + 0x60);
  17. set_segmdesc(gdt + 1004, segsiz - 1, (int) q, AR_DATA32_RW + 0x60);
  18. for (i = 0; i < datsiz; i++) {
  19. q[esp + i] = p[dathrb + i];
  20. }
  21. start_app(0x1b, 1003 * 8, esp, 1004 * 8, &(task->tss.esp0));
  22. memman_free_4k(memman, (int) q, segsiz);
  23. } else {
  24. cons_putstr0(cons, ".hrb file format error.\n");
  25. }
  26. memman_free_4k(memman, (int) p, finfo->size);
  27. cons_newline(cons);
  28. return 1;
  29. }
  30. /*没有找到文件的情况*/
  31. return 0;
  32. }

本次修改的要点如下:

  • 文件中找不到“Hari”标志则报错。

  • 数据段的大小根据.hrb文件中指定的值进行分配。

  • 将.hrb文件中的数据部分先复制到数据段后再启动程序。

我们来“make run”一下。hello4.hrb运行成功了,但不是由bim2hrb生成的hello.hrb等程序就会出错。在以后的内容中,即便使用汇编语言编写应用程序,我们也需要先生成.obj文件,然后再生成.bim并转换成.hrb。这样一来即便将文件扩展名误写为.hrb,也不会发生运行不该运行的文件的风险了。

5 用C语言显示字符串(2)(harib19e) - 图1

终于运行了

下面我们用一个例子来看看只用汇编语言编写应用程序的情形,我们写一段和hello4.c功能相同的程序。

本次的hello5.nas

  1. [FORMAT "WCOFF"]
  2. [INSTRSET "i486p"]
  3. [BITS 32]
  4. [FILE "hello5.nas"]
  5. GLOBAL _HariMain
  6. [SECTION .text]
  7. _HariMain:
  8. MOV EDX,2
  9. MOV EBX,msg
  10. INT 0x40
  11. MOV EDX,4
  12. INT 0x40
  13. [SECTION .data]
  14. msg:
  15. DB "hello, world", 0x0a, 0

将上面的程序make一下,得到78个字节的hello5.hrb,而同样内容的hello4.hrb却需要114个字节,果然还是汇编语言比较节省呢(笑)。

在WCOFF模式下的nask中必须要使用SECTION命令,这个命令是用来下达“将程序的这个部分放在代码段,将那个部分放在数据段”之类的指示(不过在.obj文件中不用“段”[segment]这个词,而是用“区”[section],比如代码段在这里要被称为文本区[text section]。为什么呢?笔者也不知道,从一开始就是这样叫的,如果你有意见的话……笔者也不知道该去找谁投诉了,不好意思啦)。

■■■■■

如果大家明白了.hrb文件中所包含的信息,那么对于asmhead.nas启动bootpack.hrb的部分,应该也会理解得更透彻了。

说段题外话。在一般操作系统的可执行文件中都会加入像“Hari”这样的标记,不过通常情况下这个标记会放在文件的开头。例如,Windows的.exe文件开头两个字节内容就是“MZ”。那么.hrb文件中的标记为什么不放在开头,而是从第4个字节开始呢?下面来讲解一下。

笔者的考虑是,如果将标记存放在文件开头,有些普通的文本文件在偶然的情况下也有可能带有与标记相同的字符,从而被误认为是可执行文件。当然,我们还会通过扩展名来进行区分,所以一般情况下也不太会弄错,但如果扩展名可靠的话,就没必要加什么标记了。正是因为通过扩展名判断有时候会出错,我们才特地加了4个字节的标记,而如果不将标记放在文件开头可以进一步减少和普通文本文件混淆的可能性的话,那为什么不这样做呢?

开头的4个字节我们用来存放数据段的大小,而bim2hrb会自动将数据段大小调整为4KB的倍数,因此低位的8个比特总是0x00,也就是说,文件开头的第1个字节肯定是“00”。普通的文本文件不可能一上来就以“00”开头,因此就不大可能将文本文件误认为成可执行文件了。

将存放“Hari”的位置推后4个字节就可以如此显著地提高安全性,这应该算是个聪明的点子吧,至少笔者自己是这么认为的(笑)。