C入门小程序
这篇文档主要设计以下几个知识点:
C语言的指针
gcc和gdb的简单使用
简单的汇编指令
Linux环境下C程序运行时的栈帧
奇怪的代码
注意 除非在使用gcc编译代码的时候说明使用了编译器优化参数-O,其它所有的gcc命令都不使用编译器优化。
刚入门写下下面的代码很常见,虽然是错误的写法(ch指针没有初始化),但是程序的却能正常的执行。
#include
int main() {
long i;
long j;
char *ch;
scanf("%s", ch);
}
但是在scanf语句下在给变量i和j进行初始化话,程序却不能正确的执行了,在执行scanf后会抛出SegmentFault异常。
#include
int main() {
long i;
long j;
char *ch;
scanf("%s", ch);
i = 0;
j = 0;
}
更奇怪的是,如果将i或者j注释掉其中的一个,程序又能正常的执行:
#include
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 : push %rbp
0x0000000000400547 : mov %rsp,%rbp
0x000000000040054a : sub $0x10,%rsp
0x000000000040054e : mov -0x10(%rbp),%rax
0x0000000000400552 : mov %rax,%rsi
0x0000000000400555 : mov $0x400604,%edi
0x000000000040055a : mov $0x0,%eax
0x000000000040055f : callq 0x400430 <__isoc99_scanf>
0x0000000000400564 : movq $0x0,-0x8(%rbp)
0x000000000040056c : mov $0x0,%eax
0x0000000000400571 : leaveq
0x0000000000400572 : retq
没有注释掉i和j的gdb调试界面,版本二:
(gdb) disassemble main
Dump of assembler code for function main:
0x0000000000400546 : push %rbp
0x0000000000400547 : mov %rsp,%rbp
0x000000000040054a : sub $0x20,%rsp
0x000000000040054e : mov -0x18(%rbp),%rax
0x0000000000400552 : mov %rax,%rsi
0x0000000000400555 : mov $0x400604,%edi
0x000000000040055a : mov $0x0,%eax
0x000000000040055f : callq 0x400430 <__isoc99_scanf>
0x0000000000400564 : movq $0x0,-0x10(%rbp)
0x000000000040056c : movq $0x0,-0x8(%rbp)
0x0000000000400574 : mov $0x0,%eax
0x0000000000400579 : leaveq
0x000000000040057a : 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)的值是上一次入栈时写入的数据,因为没有进行初始化,所以还保留了上一次操作的值(个人臆测)。