2 alloca(2)(harib25b)

即便没有__alloca,只要用malloc就可以搞定了,我们就到这里结束吧……等等,我可没有这么说哦(笑)。栈中使用的变量一多,程序就无法正常运行了,这可说不过去呀。

alloca.nas

  1. [FORMAT "WCOFF"]
  2. [INSTRSET "i486p"]
  3. [BITS 32]
  4. [FILE "alloca.nas"]
  5. GLOBAL __alloca
  6. [SECTION .text]
  7. __alloca:
  8. ADD EAX,-4
  9. SUB ESP,EAX
  10. JMP DWORD [ESP+EAX] ; 代替RET

于是我们编写了包含上述内容的alloca.nas,并将它放在了apilib中。虽然它并不能称为API,但是另外归类实在太麻烦了,我们就先放在apilib中好了。

■■■■■

这个程序实际上只有3行内容,却颇有内涵,下面我们来讲解一下。

__alloca会在下述情况下被C语言的程序调用(采用near-CALL的方式)。

  • 要执行的操作从栈中分配EAX个字节的内存空间(ESP -= EAX;)

  • 要遵守的规则不能改变ECX、EDX、EBX、EBP、ESI、EDI的值(可以临时改变它们的值,但要使用PUSH/POP来复原)

看到这里大家可能会想“什么嘛?这么简单”,于是就编写出下面这样的程序:

错误的alloca示例(1)

  1. SUB ESP,EAX
  2. RET

但这个程序是无法运行的,因为RET返回的地址保存在了ESP中,而ESP的值在这里被改变了,于是读取了错误的返回地址(注意:“RET”指令实际上相当于“POP EIP”)。

■■■■■

既然这个不行,我们又想到了别的办法。

错误的alloca示例(2)

  1. SUB ESP,EAX
  2. JMP DWORD [ESP+EAX] ; 代替RET

这个貌似不错,JMP的目标地址从[ESP]变成了[ESP+EAX],而ESP+EAX的值正好是减法运算之前的ESP值,也就是正确的地址。

不过这样还是有个问题,“RET”指令相当于“POP EIP”,而“POP EIP”实际上又相当于下面两条指令:

  1. MOV EIP,[ESP] ; 没有这个指令,用JMP [ESP]代替
  2. ADD ESP,4

也就是说,刚刚我们忘记给ESP加上4,因此ESP的值就有了误差。

那么我们再来改良一下,程序就变成了下面这样。

错误的alloca示例(3)

  1. SUB ESP,EAX
  2. JMP DWORD [ESP+EAX] ; 代替RET
  3. ADD ESP,4

这个程序的问题在于ADD指令的位置,将ADD指令放在了JMP指令的后面,所以是不可能被执行的,因此也失败了。

■■■■■

这次我们一定得解决这个问题,于是我们将程序改成了下面这样。

基本正确的alloca示例

  1. SUB ESP,EAX
  2. ADD ESP,4
  3. JMP DWORD [ESP+EAX-4] ; 代替RET

这个程序可以成功运行,太好了!因此将这个程序直接作为alloca.nas也没问题。这里的要点是:先加上4,然后在JMP指令的地址计算中再减掉4.

讲到这里,再回头看看前面实际的__alloca,怎么样,更加简短吧?不错不错。

■■■■■

这样一来sosu2.hrb应该就可以正常运行了,我们来试试看,“make run”。

2 alloca(2)(harib25b) - 图1 2 alloca(2)(harib25b) - 图2
看,成功了! 为了防止你们说我用sosu3滥竽充数,来看一下开头

运行成功,耶!

话说,sosu2.hrb和sosu3.hrb在运行结果上就没有什么区别了,那么程序的大小如何呢?

sosu2.hrb:1484字节

sosu3.hrb:1524字节

虽然差距不大,但还是sosu2.hrb更胜一筹。

既然如此,我们让winhelo也从栈中分配buf的空间吧。HariMain函数中声明的变量在程序结束前是不会被释放的,因此完全可以代替。

本次的winhelo.c

  1. #include "apilib.h"
  2. void HariMain(void)
  3. {
  4. int win;
  5. char buf[150 * 50]; /*这里!*/
  6. win = api_openwin(buf, 150, 50, -1, "hello");
  7. for (;;) {
  8. if (api_getkey(1) == 0x0a) {
  9. break; /*按下回车键则break;*/
  10. }
  11. }
  12. api_end();
  13. }

在Makefile中设定“STACK = 8k”,然后“make run”一下(buf大约需要7.5KB的空间)。哦哦,运行成功了,而且程序大小变为174字节了,要知道修改前的大小有7664字节呢(因为有“RESB 7500”),真是个重大改进。由于担心磁盘空间不够(考虑到后面我们还要加入字库),因此应用程序当然是越小越好。

2 alloca(2)(harib25b) - 图3

174字节的程序就可以实现这样的功能!

说实话,本来winhelo.hrb一开始就是从栈中为buf分配空间的,不过buf差不多要占用7.5KB,超过4KB了,没有alloca的话是不会成功运行的。如果在22.5节那个阶段就引入alloca的话,整体的条理就会被打乱,因此笔者才不得已将buf的声明放到函数外面,勉强算是挺过了那一关(C语言中,在函数外部声明的变量和带static的变量一样,都会被解释为DB和RESB;在函数内部不带static声明的变量则会从栈中分配空间)。

随后,在23.1节中由于我们引入了malloc,所以即便没有alloca也可以尽量缩小应用程序的大小,于是就没有机会讲解alloca了。在这里笔者想说的是,现在我们终于让winhelo.hrb恢复到它本来的样子了,可喜可贺呀。

下面,我们顺便将winhelo2.hrb也修改一下。

本次的winhelo2.hrb

  1. #include "apilib.h"
  2. void HariMain(void)
  3. {
  4. int win;
  5. char buf[150 * 50]; /*这里!*/
  6. win = api_openwin(buf, 150, 50, -1, "hello");
  7. api_boxfilwin(win, 8, 36, 141, 43, 3 /*黄色*/);
  8. api_putstrwin(win, 28, 28, 0 /*黑色*/, 12, "hello, world");
  9. for (;;) {
  10. if (api_getkey(1) == 0x0a) {
  11. break; /*按下回车键则break;*/
  12. }
  13. }
  14. api_end();
  15. }

大功告成。说是修改,其实也就是移动了1行代码而已。我们来“make run”一下……咦,一般保护异常?哎呀不好,忘记将STACK设定为8k了。

设定为8k之后就可以正常运行了,程序只有315字节,撒花!

改良前的winhelo2.hrb:7808字节

改良后的winhelo2.hrb:315字节

winhelo3.hrb(用malloc方式):359字节