Debug一个C语言加法程序

以下内容作者原创,欢迎指出错误,转载请注明出处~

  • Debug环境:ubuntu 17.10
  • Debug工具:GCC (Ubuntu 7.2.0-8ubuntu3.2) 7.2.0


    目录

[TOC]

加法程序源代码

//add.c
#include <stdio.h>
int main()
{
    int a,b,c;
    a=1;
    b=2;
    c=a+b;
    printf("%d",c);
}

调试过程

首先在ubuntu下建一个文件夹,然后使用新建一个add.c的文件(不必使用CB那些,直接用vim和gedit也可以),里面填上我们的代码。
>注意:要是你创建不了文件的话很可能是你的权限不足,直接使用 **chmod 777 add**就行(add是我文件夹的名称)

这里我解释下,其实我们的源程序在变成可执行程序的时候,需要经过预处理(Processing)、编译(Compilation)、汇编(Assembly)、链接(Linking)四个阶段。

预处理(Processing)

ubuntu比较简单是因为GCC直接集成在了系统的环境变量里,比较方便(好吧,我承认其实就是因为我懒,不想去配置windows下的环境变量)。在刚才新建的文件夹里打开Terminal,获得根权限,然后执行gcc -E add.c -o add.processing 解释下这段命令,gcc - E表示预处理,o是output,后面是生成预处理中间文件的名称。执行完此命令后你可以在add文件夹下找到一个名叫add.processing的文件,打开这个文件你可以看到原来几行的文件变成了一大堆看不懂的东西,emmmmm……

typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;


typedef signed char __int8_t;
typedef unsigned char __uint8_t;
typedef signed short int __int16_t;
typedef unsigned short int __uint16_t;
typedef signed int __int32_t;
typedef unsigned int __uint32_t;

typedef signed long int __int64_t;
typedef unsigned long int __uint64_t;
……

 char* _IO_read_ptr;
 char* _IO_read_end;
 char* _IO_read_base;
 char* _IO_write_base;
 char* _IO_write_ptr;
 char* _IO_write_end;
 char* _IO_buf_base;
 char* _IO_buf_end;

 char *_IO_save_base;
 char *_IO_backup_base;
 char *_IO_save_end;

 struct _IO_marker *_markers;

 struct _IO_FILE *_chain;
 ……
int main()
{
    int a,b,c;
    a=1;
    b=2;
    c=a+b;
    printf("%d",c);
}

这里粘出来了部分代码,从这里我们可以看到,预处理加上了一宏定义,然后定义了一大堆char的指针,对比了下源程序,我发现源程序里面的include不见了,再看了下本地stdio.h的内容,猜测了下是不是他把stdio的内容拷进去了?于是找了下google,度娘的解释是(不要问我为什么google看度娘,扎心):

  1. 将源文件中以”include”格式包含的文件复制到编译的源文件中。
  2. 用实际值替换用“#define”定义的字符串。
  3. 根据“#if”后面的条件决定需要编译的代码。

看了下还是挺为自己的机智所折服的哈哈哈,然后其实还进行了一些条件编译,使得预处理器按照不同的条件去编译,从而得到不同的目标代码(不是一个源程序吗,那应该执行的结果是一样的啊,为什么还要生成不同的目标代码呢,是因为编译环境的影响吗)。貌似这样就可以解释为什么C语言允许头文件相互引用了,因为一旦两者相互引用,在复制生成的时候就会像递归一样重复生成,后果不堪设想。这段字刚打完,我发现我错了,还是有多个头文件相互引用的情况,那这种情况怎么处理呢?后来发现原来还有条件编译这种东西(Cpp学过,忘了),通过#ifndef这些条件编译语句可以达到我们想要的效果。

编译(Compilation)

这里我们执行编译命令:gcc -S add.c -o add.compilation(其实这里我就有点迷了,为什么我如果用预处理过后的文件就回报warning说:linker input file unused because linking not done?难道我直接执行gcc -S默认会执行gcc -E?)。执行过后我们就会看到文件夹下多出来一个add.compilation的文件,打开后一看傻眼了:

    .file   "add.c"
    .section    .rodata
.LC0:
    .string "%d"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    subq    $16, %rsp
    movl    $1, -12(%rbp)
    movl    $2, -8(%rbp)
    movl    -12(%rbp), %edx
    movl    -8(%rbp), %eax
    addl    %edx, %eax
    movl    %eax, -4(%rbp)
    movl    -4(%rbp), %eax
    movl    %eax, %esi
    leaq    .LC0(%rip), %rdi
    movl    $0, %eax
    call    printf@PLT
    movl    $0, %eax
    leave
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 7.2.0-8ubuntu3.2) 7.2.0"
    .section    .note.GNU-stack,"",@progbits

这个难道就是传说中的汇编指令?分析一波,从上往下看:

.file

就是我们的文件

.section

汇编程序中以.开头的名称并不是指令的助记符,不会被翻译成机器指令,而是给汇编器一些特殊指示,称为汇编指示(Assembler Directive)或伪操作(Pseudo-operation),由于它不是真正的指令所以加个“伪”字。.section指示把代码划分成若干个段(Section),程序被操作系统加载执行时,每个段被加载到不同的地址,操作系统对不同的页面设置不同的读、写、执行权限。.rodata段保存程序的数据,是只读的,相当于C程序的全局变量。本程序中没有定义数据,所以.data段是空的。

.LC0

这里理解起来就比较吃力了,(问了下学过汇编的大佬,他竟然说我这不是汇编,WTF别吓我,他又说可能是平台的原因)据说这个.LC0是一个标签,说白了就是一个地址,但是后面那一坨又是些什么鬼东西,看了下,首先这个string就没怎么搞懂,.text又没搞懂,但是刚才看了一篇博客说的.text是保存代码的只读可执行区段,那就先假装是嘛。global就是从全局函数?以此类推type就说明这个main是一个函数。
然后就是这个LFB0,这个东西写在main后面,说明就是main的地址标签。学到老活到老。cfi_def_cfa cfi_endproc cfi_startproc的命令,这些前面都有个关键字cfi 是Call Frame infromation的意思。.cfi_startproc 用在每个函数的开始,用于初始化一些内部数据结构,而.cfi_endproc 在函数结束的时候使用与.cfi_startproc相配套使用。

pushq %rbp

这个我先上一张图


寄存器

就是j将rsp(堆栈基指针)压栈,pushq 指令将rsp寄存器的值减去一个指针长度,在64-bits机器上即8byte,然后将 rbp寄存器的值写入到rsp指向的地址处。

.cfi_def_cfa_offset 16

该指令表示: 此处距离CFA地址为16字节,这里的此处就是指的是前面提到的rbp。
这个CFA我又专门去查了下:

CFA(Canonical Frame Address)是标准框架地址, 它被定义为在前一个调用框架中调用当前函数时的栈顶指针。

.cfi_offset 6, -16

把第6号寄存器[5]原先的值保存在距离CFA -16的位置。

movq %rsp, %rbp

这条指令是赋值语句,把后面的值赋给前面,即把 %rbp赋给 %rsp,现在两个寄存器处在同一个位置,我觉得这里有必要上一张图(网上偷看的):

高地址
   |   | 返回地址  |
   |   +----------+
   |   | 旧的ebp   |
低地址  +----------+ <--- %rsp(%rbp) 

这里你可能有点迷,为什么要这么做,这两步操作是个规范化步骤, 叫做前序(prologue), 它有两个作用 :

  • 标记一个新的调用框架。保存前一个函数的调用框架的基址(旧的rbp), 使rbp指向当前函数的调用框架基址。

  • 在函数的执行过程中, 函数的局部变量将会是在返回地址之下的区域开辟空间来存放, 由于rbp是固定的, 可以用它作标杆, 标示参数与局部变量的位置。比如可能第一个参数位于%rbp + 8, 第二个参数位于%rbp + 12。也正是这个原因, 参数采用从右到左传递, 对实现可变参数有利: 通过%rbp + 8获取第一个参数后, 可从中获知参数个数, 然后, 依次偏移, 即可获取各个参数。

.cfi_def_cfa_register 6:

这条指令是位于movq %rsp, %rbp之后。意思是: 从这里开始, 使用rbp作为计算CFA的基址寄存器(前面用的是rsp)。

subq $16, %rsp

$:代表当前指令的地址
sub指令表示第二个参数的值减去第一个参数,这里表示将rsp减去16,即将基地址下移16个字节,就是为局部变量申请内存空间, 开辟了16字节是因为GCC的栈上默认对齐是16字节,这个是查的GCC文档。

movl $1, -12(%rbp) movl $2, -8(%rbp)

前面说了,%rbp是被调用者保护,保持不变,但是我们可以通过它来访问变量。这里将$1(就是我们赋给a的1)寻址,数字->寄存器,现在指令栈的状态就是:

高地址
   |   | 返回地址   |
   |   +----------+
   |   | 旧的ebp   |
   |   +----------+<--- %rsp(%rbp) 
   |   |          |
   |   +----------+
   |   |   b的值   |
   |   +----------+ <--- %rbp-8
   |   |   a的值   |
低地址  +----------+ <--- %rbp-12 
movl -12(%rbp), %edx movl -8(%rbp), %eax

这两句就是将我们a,b的值赋給返回值和参数。

addl %edx, %eax

这个就比较简单了,将我们上面赋好的值相加,表示将这两个地址里面的值送入寄存器,将结果保存在#eax里。

movl %eax, -4(%rbp) movl -4(%rbp), %eax

这一段看了很久还是很迷啊先赋值,然后再赋回来是什么意思

高地址
   |   | 返回地址   |
   |   +----------+
   |   | 旧的ebp   |
   |   +----------+<--- %rsp(%rbp) 
   |   |          |
   |   +----------+ <--- %rbp-4
   |   |   b的值   |
   |   +----------+ <--- %rbp-8
   |   |   a的值   |
低地址  +----------+ <--- %rbp-12 
movl %eax, %esi

这里再把 %eax 赋給参数%esi

leaq .LC0(%rip), %rdi

lea是load effective address, 加载有效地址,可以将有效地址传送到指定的的寄存器,其效果等同与C语言的&。但是这里我胖虎就不太理解了,这里前面没有向这两个空间写入地址,对他们的相互赋值有意义吗

movl $0, %eax

这个返回值?返回0,这个是哪来的啊?

call printf@PLT

函数调用,调用的是printf方法。

leave

这个指令叫尾声,说道这个名词你就会想到前面的前序,是的,这两者的刚好相反,其实他就相当于这两条语句:

movl %rbp, %rsp
pop %rbp
.cfi_def_cfa 7, 8

位于leave语句之后,现在重新定义CFA, 它的值是第7号寄存器(esp)所指位置加8字节。

ret

返回值返回

.LFE0:

它后面就是一些信息记录了,比如编译器版本……

  • CFI: 调用框架指令,,CFI全称是Call Frame Instrctions, 即调用框架指令。CFI提供的调用框架信息, 为实现堆栈回绕(stack unwiding)或异常处理(exception handling)提供了方便, 它在汇编指令中插入指令符(directive), 以生成DWARF[3]可用的堆栈回绕信息。这里列有gas(GNU Assembler)支持的CFI指令符。

  • CFA(Canonical Frame Address)是标准框架地址, 它被定义为在前一个调用框架中调用当前函数时的栈顶指针。

汇编(Assembly)

汇编就是将汇编代码转换为机器可以执行的指令。在汇编过程中,只有很少的信息丢失了,因此我们可以有反汇编器(dis-assembler)。反编译器不存在的原因是编译过程中丢失了高级语言的语法结构信息,局部变量的名字也被替换成了偏移量,因此程序一旦被编译为二进制码,就无法被还原成源代码了。
执行汇编命令:gcc -c add.c -o add.assembly

7f45 4c46 0201 0100 0000 0000 0000 0000
0100 3e00 0100 0000 0000 0000 0000 0000
0000 0000 0000 0000 e002 0000 0000 0000
0000 0000 4000 0000 0000 4000 0d00 0c00
5548 89e5 4883 ec10 c745 f401 0000 00c7
45f8 0200 0000 8b55 f48b 45f8 01d0 8945
fc8b 45fc 89c6 488d 3d00 0000 00b8 0000
0000 e800 0000 00b8 0000 0000 c9c3 2564
0000 4743 433a 2028 5562 756e 7475 2037
2e32 2e30 2d38 7562 756e 7475 332e 3229
2037 2e32 2e30 0000 1400 0000 0000 0000
017a 5200 0178 1001 1b0c 0708 9001 0000
1c00 0000 1c00 0000 0000 0000 3e00 0000
0041 0e10 8602 430d 0679 0c07 0800 0000
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0100 0000 0400 f1ff
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0300 0100 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0300 0300
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0300 0400 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0300 0500
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0300 0700 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0300 0800
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0300 0600 0000 0000 0000 0000
0000 0000 0000 0000 0700 0000 1200 0100
0000 0000 0000 0000 3e00 0000 0000 0000
0c00 0000 1000 0000 0000 0000 0000 0000
0000 0000 0000 0000 2200 0000 1000 0000
0000 0000 0000 0000 0000 0000 0000 0000
0061 6464 2e63 006d 6169 6e00 5f47 4c4f
4241 4c5f 4f46 4653 4554 5f54 4142 4c45
……

这是一堆什么鬼,但是我们可以猜测,这就是机器码。

链接(Linking)

未经链接的目标码(汇编成的文件)是不可执行的。链接就是在不同的模块间对符号进行重定位(relocation)。早在使用机器语言在穿孔纸带上写程序时,人们无法忍受手工修改模块间跳转地址的麻烦,于是就有了符号表和根据符号表做重定位的链接器。因此,链接器的历史比汇编器还要长。执行语句nm add.assembly,nm可以方便地查看目标文件中的符号(函数、变量),其中 U 表示 undefined(未定义)。

                 U _GLOBAL_OFFSET_TABLE_
0000000000000000 T main
                 U printf

后记

做到这其实基本上整个汇编过程就走了一遍了,但是其中还是有很多小问题亟待解决,很多代码还是不太明白。里面的疑惑还是很多没解决。但是通过本次尝试还是基本了解了在硬件层面上的那些指令栈的内容。放一张图片来皮一下:


x86 寄存器

注意

  1. 其实GCC在生成文件的时候不像我这么随意,后缀名其实是遵循一些规则的:

gcc所遵循的部分约定规则:
.c为后缀的文件,C语言源代码文件;
.a为后缀的文件,是由目标文件构成的档案库文件;
.C,.cc或.cxx 为后缀的文件,是C++源代码文件且必须要经过预处理;
.h为后缀的文件,是程序所包含的头文件;
.i 为后缀的文件,是C源代码文件且不应该对其执行预处理;
.ii为后缀的文件,是C++源代码文件且不应该对其执行预处理;
.m为后缀的文件,是Objective-C源代码文件;
.mm为后缀的文件,是Objective-C++源代码文件;
.o为后缀的文件,是编译后的目标文件;
.s为后缀的文件,是汇编语言源代码文件;
.S为后缀的文件,是经过预编译的汇编语言源代码文件。

  1. 由于编译环境的不同,会产生一些小的差异

参考资料

  1. 编译:一个 C 程序的艺术之旅
  2. 百度百科:GCC
  3. 通过反汇编一个简单的C程序,分析汇编代码理解计算机是如何工作的
  4. x86汇编程序基础
  5. cpp文件编译生成的汇编文件里语句的作用
  6. GCC文档
  7. 终极参考X86汇编调用框架浅析与CFI简介
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容