如下示例代码,摘录自《深入理解计算机系统》:
#include <stdio.h>
#include <stdlib.h>
size_t strlen(const char *s)
{
intlen = 0;
while (*(s++))
len++;
return len;
}
char *strcpy(char *dest, const char *src)
{
char *result = dest;
char c;
do{
c= *(src++);
*(dest++) = c;
}while (c);
return result;
}
char *getline()
{
char buf[8];
char *result;
gets(buf);
result= malloc(strlen(buf));
strcpy(result, buf);
return(result);
}
int main(int argc, char *argv[])
{
printf("Input>");
puts(getline());
return 0;
}
标黄色的代码是有问题的,如果读入的字符大于buf[8],会导致缓冲区溢出。
gcc -O1 -m32 -o bufovf bufovf.c
编译时,gcc会发出警告:
/tmp/ccWRMuOR.o: In function `getline':
bufovf.c:(.text+0x56):warning: the `gets' function is dangerous and should not be used.
objdump-d bufovf
Disassembly of section .plt:
……
080484d4 <getline>:
80484d4: 55 push %ebp
80484d5: 89 e5 mov %esp,%ebp
80484d7: 83 ec 28 sub $0x28,%esp //esp = esp – 0x28
80484da: 89 5d f4 mov %ebx,0xfffffff4(%ebp) //备份ebx
80484dd: 89 75 f8 mov %esi,0xfffffff8(%ebp) //备份esi
80484e0: 89 7d fc mov %edi,0xfffffffc(%ebp) //备份edi
80484e3: 8d 75 ec lea 0xffffffec(%ebp),%esi //esi = buf
80484e6: 89 34 24 mov %esi,(%esp) //
80484e9: e8 76 fe ff ff call 8048364 <gets@plt>//调用gets函数
80484ee: 89 f7 mov %esi,%edi //edi = esi
80484f0: fc cld //清除DF标志。
80484f1: b9 ff ff ff ff mov $0xffffffff,%ecx //ecx = -1
80484f6: b8 00 00 00 00 mov $0x0,%eax //eax = 0
80484fb: f2 ae repnz scas %es:(%edi),%al//扫描buf的内容,当寻找到al的值(0)时,扫描结束。
80484fd: f7 d1 not %ecx//ecx取反
80484ff: 83 e9 01 sub $0x1,%ecx //ecx = ecx -1,得到buf的长度。
8048502: 89 0c 24 mov %ecx,(%esp)//把长度作为入参保存到栈中
8048505: e8 8a fe ff ff call 8048394 <malloc@plt>//调用malloc
804850a: 89 c3 mov %eax,%ebx//result = eax,保存malloc分配的内存地址
804850c: 89 74 24 04 mov %esi,0x4(%esp) //esp+4 = buffer,源内存地址
8048510: 89 04 24 mov %eax,(%esp) //esp = result,目的内存地址
8048513: e8 9c ff ff ff call 80484b4 <strcpy>//调用strcpy函数
8048518: 89 d8 mov %ebx,%eax //eax = result,作为返回值
804851a: 8b 5d f4 mov 0xfffffff4(%ebp),%ebx
804851d: 8b 75 f8 mov 0xfffffff8(%ebp),%esi
8048520: 8b 7d fc mov 0xfffffffc(%ebp),%edi
8048523: 89 ec mov %ebp,%esp
8048525: 5d pop %ebp
8048526: c3 ret
……
DF(direction flag)标志位于EFLAGS寄存器第10bit位,控制字符串指令(movs,cmps, scas, lods和stos)。设置DF标志位,会引起字符串指令自减(从高字节到低字节处理),清除DF标志位,会引起字符串指令自增(从低字节到高字节处理)。
std指令:用于设置DF标志位。
cld指令:用于清除DF标志位。
repnz指令:当ecx寄存器不是0值时并且ZF标志位是0时循环。
scas指令:扫描字符串,与repnz联合使用,每循环一次,ecx减1。
es是段寄存器,保存的是附加段(extra segment)的段号。edi装载的是buf的地址偏移,通过%es:(%edi),计算出buf的虚拟内存地址(根据段号算出段起始地址,再加上edi保存的地址偏移值),读取buf的内容。
下图是getline函数调用gets(buf);前的调用栈:
从图中可以看出,如果gets函数写buf超过8个字节,就会导致缓冲区溢出,从而破坏栈上保存的数据。
如果输入的字节小于等于23(加上字符串结尾的0,小于等于24个字节),破坏保存包括ebp在内的几个寄存器的保存值,程序还能运行:
./bufovf
Input>helloworld1234567890$$$
helloworld1234567890$$$
当输入的字节数达到24个(加上字符串结尾的0,实际上是25个字节),不仅破坏掉了旧ebp值,还破坏了函数的返回地址,程序崩溃:
./bufovf
Input>helloworld1234567890$$$$
Segmentation fault