2009年1月5日星期一

GDB调试基础

GDB是一个极为强大的调试工具,绝对比那些IDE下辖的调试器强大得多。在GDB的学习过程中一个核心的问题就是Call Stack的理解,这是GDB安身立命之根本。进一步的,就要涉及到core dump的使用了。既然是只谈论GDB的基础,那么本文中就不涉及到core dump的问题,有兴趣的可以自己google一下。

准备工作
gdb需要的文件是可执行文件。要想使用gdb去调试某个可执行文件,必须在用gcc或g++生成该文件的时候使用-g选项加入调试信息。

载入可执行文件
产生了可调试的可执行文件后,必须将其载入到gdb中才可进行调试。载入文件有两种办法:

使用命令 gdb execfilename 载入;

在进入gdb后,使用

file execfilename 或是

target exec execfilename

断点管理
使用break(简写为b即可)命令设置程序断点。

break:可以在程序运行时利用frame命令选定某一个stack frame,然后执行命令break。这样使得一旦程序执行到该选中stack frame中时,就停下。

break location:其中的location可以指定为当前源文件的第几行,或是针对当前位置的偏移量,也可以指定为函数名,甚至可以指定内存中的位置。

break location if condition:这是一个条件断点设置语句。

除了break之外,还有几个断点设置的关键字:

tbreak:在程序的一次运行中只停止一次。

rbreak regex:利用正则表达式设置一系列符合regex的断点。

可以使用info命令来查看断点信息,使用disable和enable来禁用启用断点,使用clear和delete来删除指定断点。

观察点管理
使用watch命令设置程序观察点。

watch expr:其中的expr可以是参数名,也可以是表达式。当expr的值被改写的时候程序就会暂停执行。

除了watch之外,还有几个观察点设置的关键字:

rwatch:当参数或是表达式的值被程序读取的时候暂停。

awatch:当参数或是表达式的值被读取或改写的时候都会暂停。

可以使用info命令来查看断点信息,使用disable和enable来禁用启用观察点,使用delete来删除指定观察点。

设置程序运行参数
set args:可指定运行时参数。(如:set args 10 20 30 40 50);

show args:命令可以查看设置好的运行参数。

变量管理
变量值显示:print var

变量值修改:set var=value

按指定地址输出内存值:x /NFU ADDR

其中,U指定一个单元的大小,可以是b(单字节)、h(双字节)、w(四字节)、g(八字节);N表示输出的单元个数;F表示输出的数据形式,可以是x(16进制整数格式 )、d(有符号十进制整数格式)、u(无符号十进制整数格式)、f(浮点数格式 )。

调试过程控制
step:单步执行,进入函数内部;

next:单步执行,不进入函数内部;

continue:恢复正常连续执行。

信息查询
info:这个应该是调试时用得最多的信息显示命令。它的主要作用是显示正在被调试的程序的一些指定信息,如breakpoints、watchpoints,frame中的args等等。

print:一般用来打印变量的当前值。

show:显示当前gdb本身的一些参数信息。

list:一般用于源文件的显示。

Call Stack
stack frame:当某个函数被调用的时候,它的调用入口,参数,局部变量都将会被保存在栈中的一块内存中,这块内存就叫stack frame。

call stack:所有的stack frames所占据的空间就叫call stack。最近被调用的函数的stack frame总是位于call stack的最里面,依次向外扩散:最里面的stack frame编号为0.

有了以上两个概念之后,我们就可以讲述gdb调试实现的机制了。gdb工作的基本单元就是stack frame,简称frame。比如用print var来打印变量名时,gdb是会在当前选中的frame(默认是当前工作的frame,除非手动选择别的frame)中寻找并打印该变量。这就是理解gdb工作的关键!

以下是一些操作frame并显示frame信息的命令:

frame framlocation:指定gdb工作的frame。其中,framlocation可以是frame的编号,也可以是frame的内存地址。

bt:显示当前程序Call stack中的函数调用顺序,或者说是frame的顺序。命令where和info stack和bt是一样的。

info frame:显示当前frame的详细信息。

info args:显示当前frame所对应的函数的参数信息。

info locals:显示当前frame所对应的函数的局部变量信息。

执行Shell命令
直接在gdb提示符后输入 shell commandname 。而如果需要在gdb中直接make源文件的话,直接在gdb提示符后输入 make make_args 即可;也可输入shell make make_args。

gdbserver调试

in target borad
#./gdbserver 192.168.3.111:2345 voip_task VoIPCfgFile_Proxy_tulip_improved_appl.cfg

in host pc
#./gdb voip_task
(gdb) target remote 192.168.3.111:2345

2009年1月4日星期日

Linux系统下MTD/CFI驱动介绍 【转】

某些Intel的FLASH芯片(如StrataFlash系列)支持多分区,也就是各个分区可以同时进行操作。应该说这是不错的特性,但是也会带来些问题。记得当初移植Linux-2.4.21,挂JFFS2文件系统的时候,经常会报一些"Magic bitmask not found"之类的错误,跟进去发现FLASH读出来的都是些0x80之类的数据,查看资料发现该款FLASH有分区的特性,而Linux的FLASH驱动只用一个状态变量表示整个FLASH的状态,这就会造成某个分区的实际状态和系统记录的不符,从而导致读FLASH的时候该点实际上不处在读状态。当时的解决办法是,每次读的时候,不管记录的状态是什么,先进入读状态再说,当然这会带来性能的下降,具体损失多少个时钟周期就不算了。

  话说进入Linux-2.6.x的时代(具体是2.6.13),除了Lock/Unlock(Linux在擦/写的时候不先Unlock,解决办法就是初始化的时候先全部Unlock)这个老问题外,竟然多分区的错误没有出现,惊讶之下决定好好研究下Linux的MTD/FLASH驱动。

  说驱动之前,先明确几个编程要点:

  1:读写,要按照总线位宽读写,注意不是FLASH芯片位宽(例如背靠背)。

  2:寻址,程序要访问的地址和FLASH芯片地址引脚得到的值是不一样的,例如16位的FLASH芯片,对于CPU,0x00和0x01表示2个不同的字节,但是到了FLASH引脚得到的都是0,也就是都指向FLASH的第一个WORD。可以认为地址总线的bit0悬空,或者认为转换总线, bit0上实际输出的是bit1。这个解释了要点1。

  3:芯片手册提到偏移量都是基于WORD的,而WORD的位宽取决于芯片的位宽,因此在下命令的时候,实际偏移=手册偏移*buswidth/8。

  4:芯片手册提到的变量长度(典型如CFI信息)例如2,指的是,变量是个16bit数,但是读的时候,要读2个WORD,然后把每个WORD的低8位拼成1个16bit数。读WORD再拼凑确实挺麻烦,尤其是读取大结构的时候,不过参照cfi_util.c的cfi_read_pri函数的做法就简单了。

  5:背靠背,也就是比方说2块16位的芯片一起接在32位的总线上。带来的就是寻址的问题,很显然,首先要按32位读写;其次就是下命令的地址,实际偏移=手册偏移*interleave*device_type/8,device_type=buswidth/interleave,而buswidth这个时候是32(总线位宽)。另外就是背靠背的时候,命令和返回的状态码是“双份的”,例如2块16位背靠背,读命令是0x00ff00ff。

  如果不是想写像Linux那么灵活的代码(考虑各种接法/位宽/CFI获取信息等),那事情就简单很多,只要考虑要点1以及擦除块的大小就好了,当然如果是背靠背接法,擦除块的实际大小要乘个interleave。

  下面就进入Linux代码,不过关于CHIP/MAP/MTD之间绕来绕去的关系现在还糊涂着呢,因此下面只是简单的跟一下脉络和各个编程要点。

  1:构造map_info结构,指定基址/位宽/大小等信息以及"cfi_probe"限定,然后调用do_map_probe()。

  2:do_map_probe()根据名字"cfi_probe"找到芯片驱动"cfi_probe.c"直接调用cfi_probe()。

  3:cfi_probe()直接调用mtd_do_chip_probe(),传入cfi_probe_chip()函数指针。

  4:mtd_do_chip_probe()分2步,先调用genprobe_ident_chips()探测芯片信息,后调用check_cmd_set()获取和初始化芯片命令集(多分区初始化就在里面)。

  5:genprobe_ident_chips()函数如果不考虑多芯片串连的情况,那只需看前面的genprobe_new_chip()调用,完成后cfi.chipshift=cfi.cfiq->DevSize,2^chipshift=FLASH大小。

  6:genprobe_new_chip()枚举各种不同的芯片位宽和背靠背数量,结合配置设定依次调用步骤3的cfi_probe_chip(),注意cfi->device_type=bankwidth/nr_chips,bankwidth是总线位宽,device_type是芯片位宽。这里我们只需要注意有限复杂情况即可,所谓有限复杂指的是编译时确定的复杂连接。这样,cfi_probe_chip()只有第1次调用才成功,如果考虑32位宽的FLASH插在16bit总线上的情况,那第2次调用成功。

  7:cfi_probe_chip(),由于步骤6的原因,函数就在cfi_chip_setup()直接返回,后面的代码就不用考虑了。

  8:cfi_chip_setup()读取CFI信息,可以留意下Linux是怎么实现要点4的。

  9:回到步骤4的check_cmd_set()阶段,进入cfi_cmdset_0001()函数,先调用read_pri_intelext()读取Intel的扩展信息,然后调用cfi_intelext_setup()初始化自身结构。

  10:read_pri_intelext()函数,可以留意下怎么读取变长结构的技巧,也就是"need_more"的用法。这里说明下一些变量的含义,例如对于StrataFlash 128Mb Bottom类型的的FLASH芯片,块结构是4*32KB+127*128KB=16MB,一共16个分区,每个分区1MB。nb_parts=2。

  第1部分

  NumIdentPartitions=1 // 有1个重复的分区

  NumBlockTypes=2 // 分区内有2种不同的Block类型

  第1类型

  NumIdentBlocks=3 // 有4个Block(3+1)

  BlockSize=0x80 // 32KB(0x80*256)

  第2类型

  NumIdentBlocks=6 // 有7个Block(6+1)

  BlockSize=0x200 // 128KB(0x200*256)

  第2部分

  NumIdentPartitions=15// 有15个重复的分区
  NumBlockTypes=1 // 分区内有1种Block类型

  第1类型

  NumIdentBlocks=7 // 有8个Block(7+1)

  BlockSize=0x200 // 128KB(0x200*256)

  11:cfi_intelext_setup()函数首先根据CFI建立mtd_erase_region_info信息,然后调用cfi_intelext_partition_fixup()来支持分区。

  12:cfi_intelext_partition_fixup()用来建立虚拟Chip,每个分区对应1个Chip,不过并没有完全根据CFI扩展信息来建立,而是假定每个分区的大小都一致。cfi->chipshift调整为partshift,各个虚拟chip->start调整为各分区的基址。将来访问FLASH的入口函数cfi_varsize_frob()就根据ofs得到chipnum(chipnum=ofs>>cfi->chipshift),这也是为什么要假定分区一致的原因。

2009年1月1日星期四

调试技术: Linux core dump file

1. 前言:
有的程序可以通过编译, 但在运行时会出现Segment fault(段错误). 这通常都是指针错误引起的.
但这不像编译错误一样会提示到文件->行, 而是没有任何信息, 使得我们的调试变得困难起来.

2. gdb:
有一种办法是, 我们用gdb的step, 一步一步寻找.
这放在短小的代码中是可行的, 但要让你step一个上万行的代码, 我想你会从此厌恶程序员这个名字, 而把他叫做调试员.
我们还有更好的办法, 这就是core file.

3. ulimit:
如果想让系统在信号中断造成的错误时产生core文件, 我们需要在shell中按如下设置:
#设置core大小为无限
ulimit -c unlimited
#设置文件大小为无限
ulimit unlimited

这些需要有root权限, 在ubuntu下每次重新打开中断都需要重新输入上面的第一条命令, 来设置core大小为无限.

4. 用gdb查看core文件:
下面我们可以在发生运行时信号引起的错误时发生core dump了.
发生core dump之后, 用gdb进行查看core文件的内容, 以定位文件中引发core dump的行.
gdb [exec file] [core file]
如:
gdb ./test test.core
在进入gdb后, 用bt命令查看backtrace以检查发生程序运行到哪里, 来定位core dump的文件->行.