1

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语句下在给变量ij进行初始化话,程序却不能正确的执行了,在执行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   

没有注释掉ij的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))的时候两个版本会将标准输入分别写入以地址0x7fffffffddf00x400450为始的连续内存空间上。

为什么地址0x400450是非法的?来看一下Linux x86-64运行时的内存镜像:

Linux x86-64 rum-time memory image

地址0x400450在Read-only code segment区域,所以对这块地址进行写操作是非法的。在运行时环境中,只能对stackheap进行写操作。

为什么rsi地址在两个版本下的值差别这么大,一个在高地址内存空间,而另一个在低地址内存空间?在执行main函数之前,运行时已经在栈上进行了入栈出栈的操作了,地址-0x10(%rbp)-0x18(%rbp)的值是上一次入栈时写入的数据,因为没有进行初始化,所以还保留了上一次操作的值(个人臆测)。


孤独的自我
523 声望12 粉丝