using GDB/DDD/Eclipse for other languages

对其他语言使用GDB/DDD/Eclipse

       人们一般都知道GDBDDDC/C++程序的调试器,但是他们也可以用于其他语言的开发。Eclipse最初是为Java开发设计的。

       不管是CC++JavaPythonPerl还是其他可以使用这些工具的语言或调试器,如果能够使用相同的调试界面,那将是相当棒的。DDD就适用于所有这次语言

       这些工具的多语言功能是如何实现的:

l  虽然最初GDB是为了C/c++的调试器创建的,但是后来使用GNU的人也提供了一款Java的编译器GCJ

l  DDD本身不是调试器,而是GUI可以通过它来想底层调试器发布命令,对于C/c++,该底层调试器通常是GDB,然后,DDD经常可用来作为其他语言特有的调试器的前端;

l  Eclipse也只是前端,各种语言的插件赋予了它管理用那些语言编写代码的开发与调试能力。

 

DDD可以直接与Java Development KitJDB调试器结合起来使用,例如:

$ddd –jdb test.java

       Perl有它自己的内置调试器,可以通过-d选项调用:

$perl –d test.pl

       Python的基本调试器时PDB,这是一个基于文本的工具,它的有用性通过使用DDD作为GUI前端而得到大大增强。也可以通过ddd –pydb来使用。

调试SWIG代码

       SWIGSimplified Wrapper and Interface Generator)是一种流行的开源工具,用来将JavaPerlPython和若干其他解释语言与C/C++结合。大部分Linux分布式系统都包括SWIG,它允许使用解释语言编写应用程序的大部分代码,并与程序员用C/C++编写的特定部分结合,从而增强性能

汇编语言

       GDBDDD在调试汇编语言代码时也机器有用。

程序崩溃处理

程序崩溃处理

       有人说C语言是低级语言,这有一部分原因是因为应用程序的内存管理大部分需要由程序员来实现。虽然这种方法非常有用,但是也给程序员添加了很多的麻烦。

       也有人说,C语言是相对较小且容易学习的语言,然而,只有不考虑标准C语言库的典型实现时,C语言才比较小,这个库相当庞大,很多程序员认为C语言是易用语言,那是因为他们还没有遇到指针。

       一般而言,程序错误会导致下面两件事情的发生:

l  导致程序做一些程序员没有打算做的事情;

l  导致程序崩溃

 

相信很多调试过程序的兄弟都碰到过段错误即segmentation fault,则合格主要原因是试图在未经允许的情况下访问一个内存单元。硬件会感知这件事并执行对操作系统的跳转。

堆区域

       调用malloc函数分配的内存;

栈区域

       用来动态分配数据的空间,函数调用的数据(包括参数、局部变量和返回地址)都存储在栈上。

查看程序在Linux上的精确内存布局情况

       可以通过使用info proc mappings来详细查看该程序在Linux上的精确内存布局情况,例如:

clip_image002

此时我们还可以看到这个进程号为14455,所以我们还可以通过文件/proc/14455/maps来查看该信息。通过这些信息,我们有可能看到文本和数据区域,以及堆和栈。

分配页策略

       操作系统不会将不完整的页分配给程序,例如,如果要运行的程序总共大约有10000字节,如果完全加载,会占用3个内存页(一个页占4096个字节),它不会仅占用2.5个页,因为页是虚拟内存系统能够操作的最小内存单元,这是调试时要着重了解的情况,这也导致了程序的一些错误内存访问不会触发段错误,换言之,在调试会话期间,没有引起段错误并不能直接说明代码是没有问题的。

页的角色细节

       当程序执行时,它会连续访问程序中的各个区域,导致硬件按照以下几种情况所示处理页表:

l  每次程序使用全局变量时,需要具有对数据区域的读写访问权限;

l  每次程序访问局部变量时,程序会访问栈,需要对栈区域具有读写访问权限;

l  每次程序进入或离开函数时,对该栈进行一次或多次访问,需要对栈区具有读写访问权限;

l  每次程序访问通过调用malloc或者new创建的存储器时,都会发生堆访问,也需要读写访问权限;

l  程序执行的每个机器指令是从文本区域取出的,因此需要具有读和执行文件;

信号

       这里需要注意的是,进程抛出的信号,实际上没有任何内容发送给进程。所发生的事情只不过是操作系统将信号记录到进程表中,以便下次进程接收信号时得到CPU上的时间片,执行恰当的信号处理程序。

自定义信号的复杂性

       使用GDB/DDD/Eclipse调试时,自定义信号处理程序可能会使程序变得复杂,无论是直接使用还是通过DDD GUI,每当发出任何信号时,GDB都会停止进程,所以,有可能意味着GDB会因为与调试无关的工作而频繁的停止,此时可以使用handle命令告诉GDB在某些信号发生时不要停止。

总线错误的原因

l  访问不存在的物理地址;

l  在很多架构上,要求访问32位量的机器指令要求字对齐,而导致视图在奇数号地址上访问具有4字节的数的指针错误可能引起总线错误。

总线错误是处理器层的异常,导致在Unix系统上发出SIGBUS信号,默认情况下,SIGBUS会导致转储内存并终止。

核心文件

       有些信号表示让某个进程继续是不妥当的,甚至是不可能的,在这些情况中,默认动作是提前终止进程,并编写一个名为核心文件core file文件,俗称转储核心

       核心文件包含程序崩溃时对程序状态的详细描述:栈的内容、CPU寄存器的内容、程序的静态分配变量的值。

       我们可以通过file命令来查看文件的详细信息。

为什么需要核心文件

l  只有在运行了一段长时间后才发生段错误,所以在调试器中无法重新创建该错误;

l  程序的行为取决于随机的环境事件,因此再次运行程序可能不会再现段错误;

l  当新手用户运行程序时发生的段错误,需要发送核心文件给开发人员。

重载功能

       GDB注意到重新编译了程序后,它会自动加载新的可执行文件,因此不需要退出和重启GDB

调试设计的内容

l  确认原则;

l  使用核心文件进行崩溃进程的“死后”分析;

l  纠正、编译并重新运行程序后,甚至不需要退出GDB

l  Printf()风格调试的不足之处;

l  利用你的智慧,这是无可替代的;

l  如果你过去使用printf风格调试的,就会发现使用printf跟踪这些程序错误中的部分错误原来有多难,虽然在调试中使用printf诊断代码有一定的好处,但是作为一种通用目的的工具,它远远不足以用来跟踪实际代码中发生的大部分程序错误。

 

输入/输出函数

输入/输出函数

标准I/O函数库

       ANSI C的一个主要优点就是对标准函数的修改将通过增加不同函数的方法实现,而不是通过对现存函数进行修改来实现。因此,程序的可移植性不会受到影响。

对于在调试中的打印语句,可以通过添加一条fflush(stdout)语句,来强制立即输出。

流分为两种类型:文本流text和二进制流binary

字符I/O

       fgetcfputc都是真正的函数,但getcputcgetcharputchar都是通过#define指令定义的宏。宏在执行时间上效率稍高,而函数在程序的长度方面更胜一筹。

二进制I/O

       把数据写到文件效率最高的方法是用二进制形式写入。二进制输出避免了在数值转换为字符串过程中所涉及的开销和精度损失。

刷新和定位函数

fflush函数会立即把输出缓冲区中的数据进行物理写入;

ftell函数返回流的当前位置;

fseek函数允许你在一个流中定位;

rewind函数将读写指针设置回指定流的起始位置,它同时清除流的错误提示标志;fgetposfsetpos函数分贝时ftellfseek函数的替代方案。

流错误函数

几个判断流状态的函数:

l  feof:如果流当前处于文件尾,feof函数返回真;

l  ferror:报告流的错误状态,如果出现任何读写错误函数就返回真;

l  clearer:对指定流的错误标志进行重置;

文件操纵函数

removerename函数。

总结

       一种编译器可以在它的函数库中提供额外的函数,但是不应该修改标准要求提供的函数。

预处理器

预处理器

       编译一个C程序涉及很多步骤,其中第一个步骤被称为预处理preprocessing阶段。C预处理器在源代码编译之前对其进行一些文本性质的操作,它的主要任务包括删除注释、插入被#include指令包含的文件的内容、定义和替换由#define指令定义的符号以及确定代码的部分内容时候应该根据一些条件编译指令进行编译

预定义符号

符号

含义

__FILE__

进行编译的原文件名

__LINE__

文件当前行的行号

__DATE__

文件被编译的日期

__TIME__

文件被编译的时间

__STDC__

如果编译器遵循ANSI C,其值就为1,否则未定义

 

#define

       对于多行的宏定义,可以使用反斜杠来换行。

clip_image002

       我们也可以使用#define指令把一序列语句插入到程序中,例如:

clip_image004

关于宏的使用,有诸多注意的事项。比如,下面定义一个宏:

clip_image006

       所以对于上面的例子,如果我们定义如下就没有问题了:

clip_image008

 

这里再说另外一种情况:

clip_image010

对于上面的情况,我们可以用如下的方法定义(多加一个括号):

clip_image012

#define替换

       宏参数和#define定义可以包含其他#define定义的符号,但是,宏不可以出现递归

clip_image014式中的#xxx会把xxx转换为一个字符串;而##结构则执行一种不同的任务,它把位于它两边的符号链接成一个符号,比如value=5;那么#define test(x)  sum##value += x;中的sum##value会改为sum5

宏与函数

       宏非常频繁地用于执行简单的计算,比如在两个表达式中寻找其中较大或较小的一个:

clip_image016

       从上式中,我们看到通篇的括号,哈哈clip_image017,就是为了方式出现上面曾说过的错误。

       为什么不用函数来完成上面的任务呢

l  首先,用于调用和从函数返回的代码很可能比实际执行这个小型计算工作的代码更大,所以使用宏比使用函数在程序的规模和速度方面都更胜一筹

l  再者,函数的参数必须声明为一种特定的类型,所以它只能在类型合适的表达式上使用,反之,上面的这个宏可以用于整型、长整型、单浮点型、双浮点型以及其他任何可以使用>操作符比较值大小的类型,也就是说,宏是与类型无关的

l  还有一些任务根本无法用函数实现,比如,下面的type参数类型:

clip_image019

 

和使用函数相比,使用宏的不利之处在于:一份宏定义代码的拷贝都将插入到程序中,除非宏比较短,否则使用宏可能会大幅度增加程序的长度

宏和函数的不同之处

属性

#define

函数

代码长度

每次使用时,宏代码都会插入到程序中,除了非常小的宏外,程序的长度将大幅度增长

函数代码值出现在一个地方;每次使用这个函数时,都调用那个地方的同一份代码

执行速度

更快

存在函数调用/返回的额外开销

操作符优先级

宏参数的求值是在所有周围表达式的上下文环境里,除非它们加上括号,否则邻近操作符的优先级可能会产生不可预料的结果

函数参数只在函数调用时求值一次,它的结果值传递给函数,表达式的求值结果更容易预测

参数求值

参数每次用于宏定义时,它们都将重新求值,由于多次求值,具有副作用的参数可能会产生不可预测的结果

参数在函数被调用前置求值一次,在函数中多次使用参数并不会导致多种求职过程,参数的副作用并不会造成任何特殊的问题

参数类型

宏与类型无关,只要对参数的操作是合法的,它可以使用于任何参数类型

函数的参数是与类型有关的,如果参数的类型不同,就需要使用不同的函数,即使它们执行的任务是相同的。

命令行定义

如果函数中有int array[ARRAY_SIZE];可以在编译时使用-D选项来指定该值的大小,比如

gcc –DARRAY_SIZE=1000   test.c就可以将程序中的该值设置为1000

文件包含

       对于#include指令,预处理会删除这条指令,并用包含文件的内容取而代之,这样,一个头文件如果被包含到10个源文件中,它实际上被编译了10次。

       #include有两种方式<>””,其实对于标准的类似stdio.h也可以使用””来引用,只是会浪费一些时间。

       多重包含多出现在大型程序中,此时如果多重复制同一份头文件会导致程序变大变慢,解决多重包含头文件的方法为:

clip_image021

       以后每个头文件都应该加上这个条件语句。

总结

l  宏与类型无关,这是一个优点;宏的执行速度快于函数,因为宏不存在函数调用/返回的开销,但是,使用宏会增加程序的长度,函数不会;同样,具有副作用的参数可能在宏的使用过程中产生不可预料的结果,而函数参数的行为更容易预测。

l  注意在宏定义中使用的参数,要在它们周围和整个宏定义的两边加上括号;

 

高级指针话题

高级指针话题

高级声明

clip_image002返回值类型是一个指向整型的指针;

clip_image004f是一个函数指针,它做指向的函数返回一个整型值;

clip_image006f是一个函数指针,只是所指向的函数的返回值是一个整型指针,必须对其进行间接访问操作才能得到一个整型值;

clip_image008clip_image009函数只能返回标量值,不能返回数组;

clip_image010clip_image009[1]数组元素必须具有相同的长度,但不同的函数显然可能具有不同的长度;

clip_image012括号内的表达式*f[]首先进行求值,所以f是一个元素为某种类型的指针的数组。表达式末尾的()是函数调用操作符,所以f肯定是一个数组,数组元素的类型是函数指针,它所指向的函数的返回值是一个整型值。

 

对于复杂的函数定义,可以参考http://cdecl.org

clip_image014

函数指针

       函数指针的两个用途:

l  转换表jump table

l  作为参数传递给另一个函数

 

in ans;

ans = f(25);

执行流程为:函数名f首先被转换为一个函数指针,该指针指定函数在内存中的位置,然后,函数调用操作符调用该函数,执行开始于这个地址的代码。

在我们需要函数能作用与任何类型的值是,可以把参数类型声明为void *,表示一个指向未知类型的指针

转移表

对于转换表的例子为:对于一个计算器,在只是实现了加、减、乘、除时,可以使用switch语句来实现,但是如果具有成千上万个操作符,我们可以通过右边的转换表来实现。

clip_image016clip_image018

字符串常量

对于字符串“xyz”

l  “xyz”+1 是字符y的地址,因为“xyz”代表的是一个指针,即x的地址;

l  *“xyz” 代表的是x

l  “xyz”[2] 代表的是字符z

 

我们需要把十进制value转为十六进制的,可以采用:

clip_image020很帅clip_image021的方法。