目录
scanf 格式匹配引发的错误
局部变量被释放引发的 bug
数组写入超出索引维度
指针的指针引发的思考
题外话
scanf 格式匹配引发的错误
运行如下程序时, 出现这类错误:`*** stack smashing detected ***: ./test_global terminated`. 错误原因可能是因为 `scanf("%d%d", &row, &col)` 接收的是 `int` 型, 但是我使用的是 `short int`, 长度是 `Int` 的一半. 修改成 `int` 后错误消失.
- #include<stdio.h>
- int main(){
- int row, col;
- scanf("%d%d", &row, &col);
- printf("%d %d", row, col);
- return 0;
- }
使用 gcc 编译时出现的警告如下:
出现的错误如下:
局部变量被释放引发的 bug
运行如下程序时, 会无终止地打印 - 1. 原因是变量 p 所指向的变量 k 在 addr()函数执行后自行销毁, k 所使用的内存被分配给 loop()中的变量 i, 从而导致 p 指向 i. 而此时对 p 的操作是减 1, 对 i 的操作是加 1, 导致 i 的值始终为 - 1, 无法跳出循环.
- #include<stdio.h>
- void addr();
- void loop();
- long *p;
- int main(){
- addr();
- loop();
- }
- void addr(){
- long k;
- k = 0;
- p = &k;
- }
- void loop(){
- long i, j;
- j = 0;
- for (i = 0; i<10;i++){
- (*p)--;
- j++;
- printf("%d\n", i);
- }
- }
程序运行输出结果如下:
程序调试结果如下:
数组写入超出索引维度
虽然运行下面代码不会出错, 但是对数组 a[10]的写操作超出了维度, 导致在地址为 a+10 的地方也写入了数据, 但是容易引发潜在 bug.
- #include<stdio.h>
- int main()
- {
- int i;
- int a[10];
- for (i = 0; i <= 10; ++i)
- {
- a[i] = 0;
- printf("%d\n", i);
- }
- exit(0);
- }
指针的指针引发的思考
对于将指针作为参数进行传递时, 如果是将在子函数内赋值给一个新申请的空间, 那么就要注意在传递指针时, 需要传递指针的地址, 即指针的指针. 错误程序如下:
- #include<stdio.h>
- void allocateInt(int * i, int m);
- void main()
- {
- int m = 5;
- int * i = &m;
- printf("i address: %x\n", &i);
- allocateInt(i, m);
- printf("*i = %d\n", *i);
- }
- void allocateInt(int * i, int m)
- {
- printf("i address: %x\n", &i);
- i = (int *) malloc(sizeof(int));
- *i = 3;
- }
指针的指针引发的思考 -- 思考
虽然对该问题的解释一般是: 在传递参数时, 系统为子函数的变量新申请一部分空间, 因此在 void allocateInt(int * i)中, i 的地址和在 void main()中的地址是不同的, 而 void allocateInt(int * i)中的 i 是局部变量, 在子函数运行结束会被释放掉, 因此 void main()中的 i 是无法得到 malloc 的地址的, 更不可能得到新的赋值.
下面通过 gdb 调试以及反汇编来进行说明:
程序在运行至 main 函数中的 allocateInt(i, m); 语句时, 变量 i 和 m 的内存地址如下图所示,&i=0x7fffffffdaf0,&m=0x7fffffffdaec:
之后使用命令 si 对汇编语言进行单步调试, 连续运行 5 次 si 命令后(主要是保留变量 i 和 m 的值), 程序进入 allocateInt 函数. 进入时, i=0x7ffff7ffe168, m=0, 也就是说 i 和 m 还并没有被传递赋值, 结果如下所示:
但此时, 变量 i 和 m 的地址是不同的,&i=0x7fffffffdac8,&m=0x7fffffffdac4, 如下图所示:
再运行 5 次汇编指令后, 才将参数的完成传递赋值, 程序的指针才开始指向
void allocateInt(int * i, int m)
中的
printf("i address: %x\n", &i);
, 如下图所示:
此时的 i 和 m 已经被赋值, i=(int *) 0x7fffffffdaec, m=5.
针对在第 3 点提到的 4 次汇编指令, 这里进一步说明.
第 1 条指令是 push %rbp, 也就是把 rbp 寄存器入栈;
第 2 条指令是 mov %rsp,%rbp, 其中 rsp 是堆栈指针. 也就是把堆栈指针的值赋值给 rbp 寄存器;
第 3 条指令是 sub $0x10,%rsp, 也就是把堆栈指针所指向的地址减少 16 个字节. 这是因为变量 i 和 m 一共占用了 16 个字节;
第 4 条指令是
mov %rdi,-0x8(%rbp)
, 也就是把寄存器 rdi 的值 (rdi=0x7fffffffdaec, 如下图所示) 赋值给 i. 因为 i 的地址就是 rbp-0x8;
第 5 条指令是
mov %esi,-0xc(%rbp)
, 作用类似于第 4 条, 将寄存器 esi 的值 (esi=0x5, 如下图所示) 赋值给 m.
关于寄存器的相关知识, gdb 的调试命令可以参考下面的参考资料;
关于汇编指令中出现的 lea 命令可以网上查找, 主要就是一种更加有效的 mov 方法;
关于汇编指令中出现的
callq 0x4004a0 <printf@plt>
, 意思是调用 print 函数. 但是这里并不是直接调用 print 函数, 而是调用类似于 print 函数在进程中的别名. 因为这是公用库中的函数, 因此不同进程中都会调用, 所以只在进程中存留一个函数地址或者别名就好. 具体参见 Stack Overflow 上的一篇文章 What does @plt mean here?.
题外话
在编写时注意局部性原理, 提高性能. 一般 cache 会把某次访问的内存地址附近区域的内容都加载进去. 如果在编写程序时相邻语句访问的数据是在内存中连续的, 那么就会调高 cache 的命中率.
在编写时注意分支预测导致的性能问题. 在向下跳转的情况下, 优先将最有可能执行的语句放在 if 分支下, 减少分支预测时的开销(向下跳转在静态分支预测中一般默认不跳转; 向上跳转在静态分支预测中一般默认跳转), 例如:
- int a = -5;
- int b = 0;
- ................................................
- if(a> 0){ if(a <= 0){
- b = 1; b = 2;
- } }
- else{ else{
- b = 2; b=1;
- } }
关于分支预测的一些预测方式可以参考一篇博客 C++ 性能榨汁机之分支预测器
参考资料
Visual Studio 文档: 寄存器使用
探究 Linux 下参数传递及查看和修改方法
gdb 调试入门, 大牛写的高质量指南 http://blog.jobbole.com/107759/
GDB 的调试命令
What does @plt mean here?
C++ 性能榨汁机之分支预测器
来源: https://www.cnblogs.com/wFrancow/p/9850684.html