实验代码:
#include <stdio.h> #include <stdlib.h> typedef struct { int a[2]; double d; } struct_t; double fun(int i) { volatile struct_t s; s.d = 3.14; s.a[i] = 1073741824; /* Possibly out of bounds */ return s.d; /* Should be 3.14 */ } int main(int argc, char *argv[]) { int i = 0; if (argc >= 2) i = atoi(argv[1]); double d = fun(i); printf("fun(%d) --> %.10f\n", i, d); return 0; }将如上代码在Ubuntu中编译后输入数据0、1、2、3、4、5后分别得到如下结果:
可以发现当输入数据2和3时,原数据3.1400000000发生了变化,而当继续输入数据4、5时,又恢复了原数据,为何会出现这样的情况呢? 我们可以看到在:
double fun(int i) { volatile struct_t s; s.d = 3.14; s.a[i] = 1073741824; /* Possibly out of bounds */ return s.d; /* Should be 3.14 */ }这一段代码中,是由输入的i值来对s.d进行修改,而i决定了代码:
typedef struct { int a[2]; double d; } struct_t;中数组a[ ]的元素下标 而在这一段代码中,int a[2]决定了a这一数组只有两个元素,也就是a[0]和a[1],而后紧接着定义了double数据类型d,那么我们可以推论,因为在64位系统中,int型占用4字节(32位),double型占用8字节(64位),所以如上代码在计算机中的存储情况大概是这样: 这样一来当我们运行double fun(int i)时,依据我们输入的i值可以得知: 当输入0时,指向了第一个4字节区域,也就是a[0],故得出的结果没有变化; 当输入1时,指向了第二个4字节区域,也就是a[1],故得出的结果没有变化; 当输入2时,指向了第三个4字节区域,在这一区域存储着double型的d,导致了原数据的变化; 当输入3时,指向了第四个4字节区域,原数据变化,原理同上; 当输入4及之后的数据时,指向的是往后的存储区域,故结果不会有变化。
实验代码:
/* Demonstration of buffer overflow */ #include <stdio.h> #include <stdlib.h> /* Implementation of library function gets() */ char *gets(char *dest) { int c = getchar(); char *p = dest; while (c != EOF && c != '\n') { *p++ = c; c = getchar(); } *p = '\0'; return dest; } /* Read input line and write it back */ void echo() { char buf[4]; gets(buf); puts(buf); } void call_echo() { echo(); } /*void smash() { printf("I've been smashed!\n"); exit(0); } */ int main() { printf("Type a string:"); call_echo(); return 0; }将如上代码在Ubuntu中编译后输入字符串0123、012、01234后得到结果如下: 可以看到当输入01234时,发生了缓冲区溢出的情况 首先分析代码:
char *gets(char *dest) { int c = getchar(); char *p = dest; while (c != EOF && c != '\n') { *p++ = c; c = getchar(); } *p = '\0'; return dest; }这一段是库函数中gets语句的原码,可以看到其内容大致为每次输入一个字符并存储这个字符直到发生错误或按下回车键(其中int c = getchar()语句中的c为输入字符的ASCII码值),而后在代码:
void echo() { char buf[4]; gets(buf); puts(buf); }中调用了这一语句gets,故当我们输入01234时,共计输入了五个字符,并且每一个字符都被单独保存了一次,而前面定义的char buf[4]数组却只申请了4个位置,故当我们输入的字符数达到5个及以上时,必定会发生缓冲区溢出,而当我们输入012、0123这些字符数不大于4的字符串时,则不会发生溢出。
实验代码:
#include <stdio.h> #include <stdlib.h> int sq(int x) { return x*x; } int main(int argc, char *argv[]) { int i; for (i = 1; i < argc; i++) { int x = atoi(argv[i]); int sx = sq(x); printf("sq(%d) = %d\n", x, sx); } return 0; }将以上代码在Ubuntu中编译后分别输入随机数据后得到如下结果: 依据代码我们知道其大致作用为计算输入数据的平方数,在如图结果中,显然当输入12、40000时得出的结果是正确的,但是当我们再增大输入的数值时,可以看到部分结果出现了明显的错误。 这一错误的出现并不是由于代码出错,而是源于计算机的存储规律,在实验中,x被设定为int型,int型在64位系统中占4个字节,也就是32位二进制机器数,计算机在做乘法时的做法是对原机器码进行左移,两个32位数计算得到的结果也还是32位数,而当计算中出现了过于大的数值时,左移这一操作将会切去一些有效的数字,这将直接导致最终保留下来的32位数数值发生变化,再转化为十进制后自然也无法得到正确的结果了。
由以上几个实验中出现的结果错误可以看出,计算机在做计算、存储时都有自己的一套规律,我们不能拿常规数学的思路来写代码,胡乱打破规律的代码往往只能带来错误,代码一定要符合计算机自身的操作规律才能最大化健壮性。