编译过程与调用栈

首先是对代码执行预处理。预处理器(preprocessor)是一个简单的程序,它用程序员(利用预处理器指令)定义好的模式代替源代码中的模式。预处理器指令用来节省输入,增加爱代码的可读性。预处理过的代码通常存放在一个中间文件中。

编译一般分两遍进行。

首先,对于处理过的代码进行语法分析。编译器吧源代码分解成小的单元并把他们按树形结构组织起来。表达式“A+B”和“B”就是语法分析树的叶子节点。

有时候在编译的第一遍和第二遍之间使用全局优化器(global optimizer)来生成更短、更快的代码。

编译的第二遍,由代码生成器(code generater)遍历语法分析树,把树的每个节点转化成汇编语言或机器代码。如果代码生成器生成的是汇编语言,那么还必须用汇编器对其汇编。两种情况的最后结果都是生成目标模块(通常是以.o或.obj为扩展名的文件)。有时也会在第二遍中使用窥孔优化器(peephole optimizer)从相邻一段代码中查找冗余汇编语句。

连接器(linker)把一组目标模块连接成可执行程序,操作系统可以装载和运行它。当某个模块中的函数要引用另一个目标模块中的函数或变量时,由连接器来处理这些引用;这就保证了所有需要的、在编译时存在的外部函数和变量仍然存在。连接器还有添加一个特殊的目标模块来完成程序启动任务。

调用栈(Call stack):调用栈描述的是函数之间的调用关系。它由多个栈帧(Stack Frame)组成,每个栈帧对应一个未运行完的函数。栈帧中保存了函数的返回地址和局部变量,因而不仅 能够在执行完后找到正确的返回地址,还很自然的保证了不同函数间的 局部变量互不相干——因为不同函数对应着不同的栈帧。

//:swap.cpp
//call stack demonstratiion
#include<stdio.h>
void swap(int* a, int* b){
int t = *a; *a = *b; *b = t;
}


int main(){
int a = 3, b = 4;
swap(&a,&b);
printf("%d %d\n", a, b);
return 0;
}


Microsoft Windows [版本 6.1.7600]
版权所有 (c) 2009 Microsoft Corporation。保留所有权利。

第一步:编译程序
C:\Users\桑海\Desktop>gcc swap.cpp -g
生成可执行程序a.exe(Linux下是a.out)。编译选项-g告诉编译器生成调试信息。

第二步:运行gdb
这样,gdb在运行时会自动装入生成的可执行程序。
C:\Users\桑海\Desktop>gdb a.exe
GNU gdb 6.8
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-pc-mingw32"...
第三步:查看源码(list的首字母)
(gdb) l
1       #include<stdio.h>
2       void swap(int* a, int* b){
3               int t = *a; *a = *b; *b = t;
4       }
5
6       int main(){
7               int a = 3, b = 4;
8               swap(&a,&b);
9               printf("%d %d\n", a, b);
10              return 0;
第四步:加断点并运行(在第四行设置断点)
(gdb) b 4
Breakpoint 1 at 0x401338: file swap.cpp, line 4.
运行到断点处
(gdb) r
Starting program: C:\Users\桑海\Desktop/a.exe
[New thread 9068.0x2350]


Breakpoint 1, swap (a=0x22ff1c, b=0x22ff18) at swap.cpp:4
4       }
第五步:查看调用栈(用bt命令打印所有栈帧信息backtrace的简称)
(gdb) bt
#0  swap (a=0x22ff1c, b=0x22ff18) at swap.cpp:4
#1  0x0040136c in main () at swap.cpp:8
p命令打印变量值
(gdb) p a
$1 = (int *) 0x22ff1c
(gdb) p b
$2 = (int *) 0x22ff18
(gdb) p *a
$3 = 4
(gdb) p *b
$4 = 3
up命令选择上一个栈帧
(gdb) up
#1  0x0040136c in main () at swap.cpp:8
8               swap(&a,&b);
(gdb) p a
$5 = 4
(gdb)  p b
$6 = 3
(gdb) p &a
$7 = (int *) 0x22ff1c
(gdb) p &b
$8 = (int *) 0x22ff18
(gdb)
最后q命令退出gdb

递归调用栈的情况

//:tst.cpp
// recurrence
#include <stdio.h>
int f(int n)
{
	return n==0?1:f(n-1)*n;
}
int main()
{
	printf("%d\n", f(3));
	return 0;
}

Microsoft Windows [版本 6.1.7600]
版权所有 (c) 2009 Microsoft Corporation。保留所有权利。


C:\Users\桑海\Desktop>gcc tst.cpp -g


C:\Users\桑海\Desktop>gdb a.exe
GNU gdb 6.8
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-pc-mingw32"...
在函数f处设置断点
(gdb) b f
Breakpoint 1 at 0x40131e: file tst.cpp, line 4.
运行
(gdb) r
Starting program: C:\Users\桑海\Desktop/a.exe
[New thread 8848.0x1fd4]


Breakpoint 1, f (n=3) at tst.cpp:4
4               return n==0?1:f(n-1)*n;
s命令单步执行
(gdb) s


Breakpoint 1, f (n=2) at tst.cpp:4
4               return n==0?1:f(n-1)*n;
(gdb)


Breakpoint 1, f (n=1) at tst.cpp:4
4               return n==0?1:f(n-1)*n;
(gdb)


Breakpoint 1, f (n=0) at tst.cpp:4
4               return n==0?1:f(n-1)*n;
(gdb)
5       }
查看调用栈帧情况
(gdb) bt
#0  f (n=0) at tst.cpp:5
#1  0x00401330 in f (n=1) at tst.cpp:4
#2  0x00401330 in f (n=2) at tst.cpp:4
#3  0x00401330 in f (n=3) at tst.cpp:4
#4  0x00401357 in main () at tst.cpp:8
(gdb) s
5       }
(gdb) bt
#0  f (n=1) at tst.cpp:5
#1  0x00401330 in f (n=2) at tst.cpp:4
#2  0x00401330 in f (n=3) at tst.cpp:4
#3  0x00401357 in main () at tst.cpp:8
单步执行
(gdb) s
5       }
(gdb) bt
#0  f (n=2) at tst.cpp:5
#1  0x00401330 in f (n=3) at tst.cpp:4
#2  0x00401357 in main () at tst.cpp:8
(gdb) s
5       }
(gdb) bt
#0  f (n=3) at tst.cpp:5
#1  0x00401357 in main () at tst.cpp:8
(gdb) s
6
main () at tst.cpp:9
9               return 0;
(gdb) bt
#0  main () at tst.cpp:9
(gdb) s
10      }
(gdb)

段错误与栈溢出

上述函数f中把3换成10000000时没有输出,-g编译后用gdb在载入,用r命令执行得:

(gdb) r
Starting program: C:\Users\桑海\Desktop/a.exe
[New thread 1792.0x714]

Program received signal SIGSEGV, Segmentation fault.
0x00401328 in f (n=99934856) at tst.cpp:4
4               return n==0?1:f(n-1)*n;

这要从编译后产生的可执行文件的构成说起:

用size命令得到:


C:\Users\桑海\Desktop>size a.exe
   text    data     bss     dec     hex filename
   3076     772      96    3944     f68 a.exe


此结果表明a.exe由正文段、数据段和bss段组成,总大小是3944,用十六进制表示 为f68。在可执行文件中正文段(Text segment)存储指令,数据段)Data Segment(存储已初始化的全局变量,BSS段(BSS Segment)存储未赋值的全局变量所需的空间。

其中调用栈并不在可执行文件汇中,而是在运行时动态创建的,里面保存着函数的调用关系和局部变量。调用栈所在的段称为堆栈段(Stack Segment)。和其他段一样,他也有自己的大小,不能被越界访问,否则就会出现段错误(Segmentation Fault)。

这样,前面的错误就不难理解了:每次调用都需要往调用栈里增加一个栈帧,久而久之就越界了。术语叫做栈溢出(Stack Overflow)。

栈空间的大小与操作系统有关。Linux中,栈并没有存储在可执行文件中,其大小是由系统命令ulimit指定的,例如ulimit -a显示当前栈的大小,而ulimit -s 32768将把栈大小指定为32MB。但在Windows中,栈大小是存储在可执行文件的。使用gcc可以指定可执行文件的栈的大小:gcc -Wl, --stack = 16777216,这样栈大小就变为16MB。

局部变量也存放在堆栈段,栈溢出不见得是递归调用太多,也可能是局部变量太大。只要总大小超过了允许的范围,就会产生栈溢出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值