gets函数在使用中有很大的漏洞,在获得字符时gets函数以EOF||’\n’作为结束标志当字符串过多时会发生缓冲区溢出。用以下代码和gets的原码来理解gets函数的缓冲区溢出问题。 代码:
#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]; /* Way too small! */ 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; }函数gets()就是我们经常在C语言中使用的库函数的源码,用以得到从键盘输入的字符。函数echo()调用函数gets()并把从键盘输入的字符存入数组buf[]。函数call_echo()调用echo(),输出数组中的字符。其实可以不用这个函数,直接在主函数中调用echo(),这样做是为了程序的美观和条理。在运行中会出现溢出问题。 当输入字符串长度小于等于4时程序能够正常运行,当输入字符串长度大于4时程序运行结果会出现 :已放弃(核心已转储)。这是因为gets函数以 EOF||’\n’作为结束标志,并不会检验字符串的长度。由于最先定义的数组长度为4,分配的存储空间为1*4个字节,当超过4个字节时就会发生缓冲区溢出问题。
设两个浮点数 x 和 y: 则浮点数加减运算结果为:
1 对阶:首先要把指数位(阶码)调成一样,并相应的使M移位,由于有效域左移会引起 最高有效位丢失,误差大,所以采用右移,此时阶码要增加。所以对阶原则是:小阶向大阶看齐。 2 有效数加减:简单的无符号数字相加减。 3 规格化:有效数求和结果可能大于1,那么就向右规格化:尾数右移1位,阶码加1。 4舍入:对于右移出去的位,采取舍入我们可以写一个函数来加深对浮点数加减法的理解:
#include <stdio.h> #include <stdlib.h> #include <string.h> #define BUFSIZE 256 int main(int argc, char *argv[]) { char prefix[BUFSIZE]; char next[BUFSIZE]; int i; float sum = 0.0; for (i = 1; i < argc; i++) { float x = atof(argv[i]); sum += x; if (i == 1) { sprintf(prefix, "%.4g", x); } else { sprintf(next, " + %.4g", x); strcat(prefix, next); printf("%s = %.4g\n", prefix, sum); } } return 0; }运行如下: 3.14的定点浮点数表示是:11.00100011110101110000101。 1e19 的双精度浮点数表示是:0 10000111110 0001010110001110010001100000100100010011110100000000 小阶向大阶看齐,3.14的阶是1,M需要右移62位,而M的精度才52,可想而知M就是0了。那么 3.14 + 1e19 的结果就是 1e19。 所以在计算过程中交换两个浮点数的位置可就会导致舍入问题,而得到不一样的结果。
定长数组的长度已经确定,但有时在使用的时候忘记,使用了超出定义的长度,这就会出现数组的溢出,覆盖其他数据,造成数据错误,丢失。在这里我们将数组定义在结构体中,利用下面这个程序直观感受数组的溢出覆盖其它数据。代码如下:
#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; }这里我们可以先回忆一下主函数带参数的情况。在main(int argc, char *argv[])中argc存放参数个数,argv[]存放参数,由于gcc运行程序时总要命令a.out,故a.out当作第一个参数放在argv[0]中,后面参数依次存放。 回到这个程序,在结构体中定义了一个可以存放两个元素的整型数组a[],长整型d,一共占据128个字节。 在栈中的储存: 每次运行这个程序a[i]==1073741824,这个数转换为二进制浮点数后前大部分位与3.14二进制浮点数相同。每次输出s.d。 运行这个程序会得到以下结果: 可以看到当i=0或1时,程序正常运行,当i=2和3时,程序结果不正确,当i>=4时程序结果又好像正确。这是因为:本来为数组a[2]分配了64个字节,当输入参数大于1时a[2],a[3]溢出并覆盖了double d的数据,d仅占64个字节,当i>=4时在d后面分配空间,不会覆盖d的数据,但此种情况仍是出错的。
浮点数,整数的表数范围是有限的,因此当数值的量级过大,就会发生舍入,溢出的问题。有以下代码来直观理解。
#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; }这是一个简单的计算平方和的程序,一次一次的加大参数的值就会发现出现错误的值。运行结果如下: 这是因为int型为32位,最大能表示的数为2^31-1=2147483647超过这个数的就会溢出。所以为了避免溢出就要了解处理问题的规模,尽量估算运算结果的可能取值范围,选择范围更大的变量类型,不要让变量的值超过该变量的取值范围。 **
递归引起的栈溢出,就是如果一个程序结束了,那么栈中的变量是会被清空的,但是递归中的变量值并没有被清空,每调用一次,函数的参数、局部变量等信息就压一次栈,当递归的层级比较深时,最终导致栈溢出。 函数调用栈时的储存结构: 下面是一个深度递归的程序,函数作用是打印数组中每个元素的地址,用递归实现。
#include <stdio.h> #include <stdlib.h> int recurse(int x) { int a[1<<15]; /* 4 * 2^15 = 64 KiB */ printf("x = %d. a at %p\n", x, a); a[0] = (1<<14)-1; a[a[0]] = x-1; if (a[a[0]] == 0) return -1; return recurse(a[a[0]]) - 1; } int main(int argc, char *argv[]) { int x = 100; if (argc > 1) x = atoi(argv[1]); int v = recurse(x); printf("x = %d. recurse(x) = %d\n", x, v); return 0; } 运行结果如下: x = 100. a at 0xbfd3f8b0 x = 99. a at 0xbfd1f890 x = 98. a at 0xbfcff870 x = 97. a at 0xbfcdf850 x = 96. a at 0xbfcbf830 x = 95. a at 0xbfc9f810 x = 94. a at 0xbfc7f7f0 x = 93. a at 0xbfc5f7d0 x = 92. a at 0xbfc3f7b0 x = 91. a at 0xbfc1f790 x = 90. a at 0xbfbff770 x = 89. a at 0xbfbdf750 x = 88. a at 0xbfbbf730 x = 87. a at 0xbfb9f710 x = 86. a at 0xbfb7f6f0 x = 85. a at 0xbfb5f6d0 x = 84. a at 0xbfb3f6b0 x = 83. a at 0xbfb1f690 x = 82. a at 0xbfaff670 x = 81. a at 0xbfadf650 x = 80. a at 0xbfabf630 x = 79. a at 0xbfa9f610 x = 78. a at 0xbfa7f5f0 x = 77. a at 0xbfa5f5d0 x = 76. a at 0xbfa3f5b0 x = 75. a at 0xbfa1f590 x = 74. a at 0xbf9ff570 x = 73. a at 0xbf9df550 x = 72. a at 0xbf9bf530 x = 71. a at 0xbf99f510 x = 70. a at 0xbf97f4f0 x = 69. a at 0xbf95f4d0 x = 68. a at 0xbf93f4b0 x = 67. a at 0xbf91f490 x = 66. a at 0xbf8ff470 x = 65. a at 0xbf8df450 x = 64. a at 0xbf8bf430 x = 63. a at 0xbf89f410 x = 62. a at 0xbf87f3f0 x = 61. a at 0xbf85f3d0 x = 60. a at 0xbf83f3b0 x = 59. a at 0xbf81f390 x = 58. a at 0xbf7ff370 x = 57. a at 0xbf7df350 x = 56. a at 0xbf7bf330 x = 55. a at 0xbf79f310 x = 54. a at 0xbf77f2f0 x = 53. a at 0xbf75f2d0 x = 52. a at 0xbf73f2b0 x = 51. a at 0xbf71f290 x = 50. a at 0xbf6ff270 x = 49. a at 0xbf6df250 x = 48. a at 0xbf6bf230 x = 47. a at 0xbf69f210 x = 46. a at 0xbf67f1f0 x = 45. a at 0xbf65f1d0 x = 44. a at 0xbf63f1b0 x = 43. a at 0xbf61f190 x = 42. a at 0xbf5ff170 x = 41. a at 0xbf5df150 x = 40. a at 0xbf5bf130 x = 39. a at 0xbf59f110 x = 38. a at 0xbf57f0f0 段错误 (核心已转储)可以看到在前大部分程序运行没有错误 ,但在递归深度加深后,程序出现了错误,这是因为每一层递归累积起来把栈空间撑爆了,出现错误,因此对递归的使用要更加审慎,尤其类似大数据项目的测试中,每一个递归都应该仔细考量其规模,因为大数据的文件系统往往在深 度、广度上都不是普通文件系统可以比拟的,单机上不会出现的问题,到了大数据上都有可能成为问题。
在cs:app(第三版)中第二节中需要很多字节,强制转换得的内容,通过下面的代码来加深了解在计算机内部这些数,字符是怎样存储的,强制转换后又有怎样的变化。下面的函数能帮助理解。 主函数:
int main(int argc, char *argv[]) { int val = 12345; if (argc > 1) { val = strtol(argv[1], NULL, 0); printf("calling test_show_bytes\n"); test_show_bytes(val); } else { printf("calling show_twocomp\n"); show_twocomp(); printf("Calling simple_show_a\n"); simple_show_a(); printf("Calling simple_show_b\n"); simple_show_b(); printf("Calling float_eg\n"); float_eg(); printf("Calling string_ueg\n"); string_ueg(); printf("Calling string_leg\n"); string_leg(); } return 0; }1、整数,浮点数的储存
#include <stdio.h> #include <stdlib.h> #include <string.h> typedef unsigned char *byte_pointer void show_bytes(byte_pointer start, size_t len) { size_t i; for (i = 0; i < len; i++) printf("%p\t0x%.2x\n", &start[i], start[i]); printf("\n"); } void show_int(int x) { show_bytes((byte_pointer) &x, sizeof(int)); } void show_float(float x) { show_bytes((byte_pointer) &x, sizeof(float)); } void show_pointer(void *x) { show_bytes((byte_pointer) &x, sizeof(void *)); } void test_show_bytes(int val) { int ival = val; //float fval = (float) ival; double fval = (double) ival; int *pval = &ival; printf("Stack variable ival = %d\n", ival); printf("(int)ival:\n"); show_int(ival); printf("(float)ival:\n"); show_float(fval); printf("&ival:\n"); show_pointer(pval); } 运行结果为: gec@ubuntu:/mnt/hgfs/share/csapp_code$ ./a.out 1073741824 calling test_show_bytes Stack variable ival = 1073741824 (int)ival: 0xbfd40020 0x00 0xbfd40021 0x00 0xbfd40022 0x00 0xbfd40023 0x40 (float)ival: 0xbfd40020 0x00 0xbfd40021 0x00 0xbfd40022 0x80 0xbfd40023 0x4e &ival: 0xbfd40020 0x34 0xbfd40021 0x00 0xbfd40022 0xd4 0xbfd40023 0xbf我们输入参数1073741824(整数1073741824的16进制表示40000000,浮点数表示4e800000)所以按照小端法会得到这样的运行结果。 2、字符串的存储
void string_ueg() { /* $begin show-ustring */ const char *s = "ABCDEF"; show_bytes((byte_pointer) s, strlen(s)); /* $end show-ustring */ } void string_leg() { /* $begin show-lstring */ const char *s = "abcdef"; show_bytes((byte_pointer) s, strlen(s)); /* $end show-lstring */ } 运行结果 Calling string_ueg 0x8048940 0x41 0x8048941 0x42 0x8048942 0x43 0x8048943 0x44 0x8048944 0x45 0x8048945 0x46 Calling string_leg 0x8048947 0x61 0x8048948 0x62 0x8048949 0x63 0x804894a 0x64 0x804894b 0x65 0x804894c 0x66这里就打打印了ABCDEF和abcdef的ascii码A是65转换为16进制为ox41,同理a的ascii码为97转换16进制ox61.
3、整数转换为浮点数
void float_eg() { int x = 3490593; float f = (float) x; printf("For x = %d\n", x); show_int(x); show_float(f); x = 3510593; f = (float) x; printf("For x = %d\n", x); show_int(x); show_float(f); } 运行结果: Calling float_eg For x = 3490593 0xbfa18b30 0x21 0xbfa18b31 0x43 0xbfa18b32 0x35 0xbfa18b33 0x00 0xbfa18b30 0x84 0xbfa18b31 0x0c 0xbfa18b32 0x55 0xbfa18b33 0x4a For x = 3510593 0xbfa18b30 0x41 0xbfa18b31 0x91 0xbfa18b32 0x35 0xbfa18b33 0x00 0xbfa18b30 0x04 0xbfa18b31 0x45 0xbfa18b32 0x56 0xbfa18b33 0x4a将x3490593展开为二进制,其值为0000 0000 0011 0101 0100 0011 0010 0001,其十六进制即为0x00354321。 因为要转化为float型,所以首先要对上述二进制的表示形式改变为 M * 2^E 的形式.由于该数明显大于1,所以按照IEEE的标准,其浮点形势必然为规格化值。因此 ,转化后的形式为 a = 1.101010100001100100001 * 2^21
根据 规格化值的定义,M = 1 + f. 所以f = 0.101010100001100100001.因为float型变量的小数域一共23位。所以b的最后23位可以得出,其值为10101010000110010000100
下面再演绎指数域的值:因为a的指数表示法中,指数E = 21。根据公式2,e = E + (2^7 -1) = 148.所以可以得出b的指数域的二进制表示为:10010100。在加上原数为正,所以符号位s=0。
所以,可以得出b的二进制表示为0 10010100 10101010000110010000100。转化为十六位进制则是0x4A550C84。 4、相反数的存储
void show_twocomp() { /* $begin show-twocomp */ short x = 12345; short mx = -x; show_bytes((byte_pointer) &x, sizeof(short)); show_bytes((byte_pointer) &mx, sizeof(short)); /* $end show-twocomp */ } 运行结果: calling show_twocomp 0xbfa18b4c 0x39 0xbfa18b4d 0x30 0xbfa18b4e 0xc7 0xbfa18b4f 0xcfshort型12345转换为16进制为ox3039二进制0011000000111001,它的相反数是各位取反再+1后1100111111000110(oxcfc7)。