2 alloca(2)(harib25b)
即便没有__alloca,只要用malloc就可以搞定了,我们就到这里结束吧……等等,我可没有这么说哦(笑)。栈中使用的变量一多,程序就无法正常运行了,这可说不过去呀。
alloca.nas
[FORMAT "WCOFF"]
[INSTRSET "i486p"]
[BITS 32]
[FILE "alloca.nas"]
GLOBAL __alloca
[SECTION .text]
__alloca:
ADD EAX,-4
SUB ESP,EAX
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)
SUB ESP,EAX
RET
但这个程序是无法运行的,因为RET返回的地址保存在了ESP中,而ESP的值在这里被改变了,于是读取了错误的返回地址(注意:“RET”指令实际上相当于“POP EIP”)。
■■■■■
既然这个不行,我们又想到了别的办法。
错误的alloca示例(2)
SUB ESP,EAX
JMP DWORD [ESP+EAX] ; 代替RET
这个貌似不错,JMP的目标地址从[ESP]变成了[ESP+EAX],而ESP+EAX的值正好是减法运算之前的ESP值,也就是正确的地址。
不过这样还是有个问题,“RET”指令相当于“POP EIP”,而“POP EIP”实际上又相当于下面两条指令:
MOV EIP,[ESP] ; 没有这个指令,用JMP [ESP]代替
ADD ESP,4
也就是说,刚刚我们忘记给ESP加上4,因此ESP的值就有了误差。
那么我们再来改良一下,程序就变成了下面这样。
错误的alloca示例(3)
SUB ESP,EAX
JMP DWORD [ESP+EAX] ; 代替RET
ADD ESP,4
这个程序的问题在于ADD指令的位置,将ADD指令放在了JMP指令的后面,所以是不可能被执行的,因此也失败了。
■■■■■
这次我们一定得解决这个问题,于是我们将程序改成了下面这样。
基本正确的alloca示例
SUB ESP,EAX
ADD ESP,4
JMP DWORD [ESP+EAX-4] ; 代替RET
这个程序可以成功运行,太好了!因此将这个程序直接作为alloca.nas也没问题。这里的要点是:先加上4,然后在JMP指令的地址计算中再减掉4.
讲到这里,再回头看看前面实际的__alloca,怎么样,更加简短吧?不错不错。
■■■■■
这样一来sosu2.hrb应该就可以正常运行了,我们来试试看,“make run”。
![]() | ![]() | |
看,成功了! | 为了防止你们说我用sosu3滥竽充数,来看一下开头 |
运行成功,耶!
话说,sosu2.hrb和sosu3.hrb在运行结果上就没有什么区别了,那么程序的大小如何呢?
sosu2.hrb:1484字节
sosu3.hrb:1524字节
虽然差距不大,但还是sosu2.hrb更胜一筹。
既然如此,我们让winhelo也从栈中分配buf的空间吧。HariMain函数中声明的变量在程序结束前是不会被释放的,因此完全可以代替。
本次的winhelo.c
#include "apilib.h"
void HariMain(void)
{
int win;
char buf[150 * 50]; /*这里!*/
win = api_openwin(buf, 150, 50, -1, "hello");
for (;;) {
if (api_getkey(1) == 0x0a) {
break; /*按下回车键则break;*/
}
}
api_end();
}
在Makefile中设定“STACK = 8k”,然后“make run”一下(buf大约需要7.5KB的空间)。哦哦,运行成功了,而且程序大小变为174字节了,要知道修改前的大小有7664字节呢(因为有“RESB 7500”),真是个重大改进。由于担心磁盘空间不够(考虑到后面我们还要加入字库),因此应用程序当然是越小越好。
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
#include "apilib.h"
void HariMain(void)
{
int win;
char buf[150 * 50]; /*这里!*/
win = api_openwin(buf, 150, 50, -1, "hello");
api_boxfilwin(win, 8, 36, 141, 43, 3 /*黄色*/);
api_putstrwin(win, 28, 28, 0 /*黑色*/, 12, "hello, world");
for (;;) {
if (api_getkey(1) == 0x0a) {
break; /*按下回车键则break;*/
}
}
api_end();
}
大功告成。说是修改,其实也就是移动了1行代码而已。我们来“make run”一下……咦,一般保护异常?哎呀不好,忘记将STACK设定为8k了。
设定为8k之后就可以正常运行了,程序只有315字节,撒花!
改良前的winhelo2.hrb:7808字节
改良后的winhelo2.hrb:315字节
winhelo3.hrb(用malloc方式):359字节