C入门小程序
这篇文档主要设计以下几个知识点:
- C语言的指针
- gcc和gdb的简单使用
- 简单的汇编指令
- Linux环境下C程序运行时的栈帧
奇怪的代码
注意 除非在使用gcc编译代码的时候说明使用了编译器优化参数-O
,其它所有的gcc命令都不使用编译器优化。
刚入门写下下面的代码很常见,虽然是错误的写法(ch指针没有初始化),但是程序的却能正常的执行。
#include <stdio.h>
int main() {
long i;
long j;
char *ch;
scanf("%s", ch);
}
但是在scanf
语句下在给变量i
和j
进行初始化话,程序却不能正确的执行了,在执行scanf
后会抛出SegmentFault异常。
#include <stdio.h>
int main() {
long i;
long j;
char *ch;
scanf("%s", ch);
i = 0;
j = 0;
}
更奇怪的是,如果将i
或者j
注释掉其中的一个,程序又能正常的执行:
#include <stdio.h>
int main() {
long i;
long j;
char *ch;
scanf("%s", ch);
i = 0;
//j = 0;
}
DEBUG
为了搞明白为什么会出现这种情况,需要使用gdb来调试这两段代码。
gcc -O0 -g main.c -o main.o
gdb main.o
进入gdb调试界面,首选使用disassemble
来反汇编函数main
,这样可以帮助设置断点和查看编译后的汇编指令。
注释掉j
的gdb调试界面,版本一:
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000400546 <+0>: push %rbp
0x0000000000400547 <+1>: mov %rsp,%rbp
0x000000000040054a <+4>: sub $0x10,%rsp
0x000000000040054e <+8>: mov -0x10(%rbp),%rax
0x0000000000400552 <+12>: mov %rax,%rsi
0x0000000000400555 <+15>: mov $0x400604,%edi
0x000000000040055a <+20>: mov $0x0,%eax
0x000000000040055f <+25>: callq 0x400430 <__isoc99_scanf@plt>
0x0000000000400564 <+30>: movq $0x0,-0x8(%rbp)
0x000000000040056c <+38>: mov $0x0,%eax
0x0000000000400571 <+43>: leaveq
0x0000000000400572 <+44>: retq
没有注释掉i
和j
的gdb调试界面,版本二:
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000400546 <+0>: push %rbp
0x0000000000400547 <+1>: mov %rsp,%rbp
0x000000000040054a <+4>: sub $0x20,%rsp
0x000000000040054e <+8>: mov -0x18(%rbp),%rax
0x0000000000400552 <+12>: mov %rax,%rsi
0x0000000000400555 <+15>: mov $0x400604,%edi
0x000000000040055a <+20>: mov $0x0,%eax
0x000000000040055f <+25>: callq 0x400430 <__isoc99_scanf@plt>
0x0000000000400564 <+30>: movq $0x0,-0x10(%rbp)
0x000000000040056c <+38>: movq $0x0,-0x8(%rbp)
0x0000000000400574 <+46>: mov $0x0,%eax
0x0000000000400579 <+51>: leaveq
0x000000000040057a <+52>: retq
观察两段代码的汇编指令。只有在0x000000000040054e
处代码不一样:
-
mov -0x10(%rbp),%rax
表示将地址%rbp-0x10
处的值传递到%rax
寄存器 -
mov -0x18(%rbp),%rax
表示将地址%rbp-0x18
处的值传递到%rax
寄存器
然后在执行mov %rax %rsi
,表示将寄存器$rxa
的值传递给寄存器%rsi
,%rsi
表示函数调用时的第二个参数,当调用scanf("%s", ch)
的时候,ch
的值就是%rsi
的值。既然程序执行的时候在scanf
报错,那么源头就是%rsi
的值不一样。那么就继续回到gdb界面,在调用scanf
的之前设置断点(这里选择地址0x0000000000400552
处),观察一下%rsi
的值到底是什么。
首选设置断点,然后执行程序:
(gdb) b *0x000000000040055f
(gdb) run
然后执行info register rsi
指令,来打印$rsi
的值:
rsi 0x7fffffffddf0 140737488346608
rsi 0x400450 4195408
程序执行的结果表面,地址0x7fffffffddf0
是合法的,0x400450
是非法的。在执行0x000000000040055f
(scanf("%s", ch)
)的时候两个版本会将标准输入分别写入以地址0x7fffffffddf0
和0x400450
为始的连续内存空间上。
为什么地址0x400450
是非法的?来看一下Linux x86-64运行时的内存镜像:
地址0x400450
在Read-only code segment区域,所以对这块地址进行写操作是非法的。在运行时环境中,只能对stack
和heap
进行写操作。
为什么rsi
地址在两个版本下的值差别这么大,一个在高地址内存空间,而另一个在低地址内存空间?在执行main
函数之前,运行时已经在栈上进行了入栈出栈的操作了,地址-0x10(%rbp)
和-0x18(%rbp)
的值是上一次入栈时写入的数据,因为没有进行初始化,所以还保留了上一次操作的值(个人臆测)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。