给大家带来探究 Gdb7.0 的新特征反向调试 (reverse debug)
如果你学不好的话,你在linux中开发的机会就很少,或者说几乎没有,它的优势就消失了,然后随着时间的流逝,你就会全部忘记她;弁言 GDB7.0是2009年10月份正式公布的。和多半程序员一样,那则动静其实不曾引发我的注重,由于gdb为数未几的几个新版本都让人以为十分平平。没有让人奋发的新特征。
一晃几个月已往了,随便扫瞄gdb主页的时分,我俄然发明一个叫做反向调试(reversedebug)的特征,冷静地列在不有目共睹的中央。”反向调试”?我们调试老是下一步,下一步,反向调试就是上一步,上一步了?
经由复杂的试用,我发明这是一个十分有效的特征,值得我们往进修和把握。使用该功效,您可让被调试历程反向实行。您大概会问,这有甚么用途呢?
嗯,我以为软件调试常常是一个推测的历程,一样平常的一般人仿佛不太大概第一次就将断点设置在最准确的地位,以是我们常常会发明主要的代码实行路径已错过。好比运转到断点之前,程序的某些形态就已不准确了。以往,我们只好加入以后调试会话,重新再来。
每次毛病的推测都必需让统统重新再来,假如您的命运欠安,很快就会以为十分抓狂吧。
假设有反向调试功效,在这类情形下,我们不必从头启动调试程序,只需复杂地让被调试历程反向实行到我们嫌疑的中央,假如这里没有成绩,我们还能够持续正向实行调试,云云这般。好像我们在进修英语时利用复读机一样,往返将听不懂的部分重放,剖析。这无疑将极年夜地进步事情效力。
反向调试的利用简介
反向调试的基础道理为record/replay。将被调试历程的实行历程录制上去,然后即可以像DVD一样的恣意反向或正向回放。因而,为了利用反向调试,您起首必要利用record命令举行录制。
今后的调试历程和您之前所熟习的历程一样,不外您如今多了几个反向把持的命令:
表1.反向调试基础命令
CommandnamedescriptionReverse-continue(rc)ContinueprogrambeingdebuggedbutrunitinreverseReverse-finishExecutebackwarduntiljustbeforetheselectedstackframeiscalledReverse-next(rn)Stepprogrambackward,proceedingthroughsubroutinecalls.Reverse-nexti(rni)Stepbackwardoneinstruction,butproceedthroughcalledsubroutines.Reverse-step(rs)StepprogrambackwarduntilitreachesthebeginningofapreviouslineReverse-stepiStepbackwardexactlyoneinstructionsetexec-directionSetdirectionofexecution.
让我们假定您已利用”break10”命令在程序的第10行设置断点。然后输出”run”让历程入手下手实行。不久,程序会停息在第10行代码处,把持权前往gdb,并守候您的命令。此时假如您利用next命令,那末程序会按次实行第11行代码,假如您利用reverse-next,那末程序将回退实行第9行代码。
利用反向调试命令以后,任什么时候候,您还能够自在地利用其他的gdb命令。好比print来打印变量的值等等。十分便利。
反向调试的完成道理
除利用这项特征所带来的优点以外,也许更引人入胜的是该特征的完成道理吧。我们都未曾见过期光倒流,可以转头实行指令的处置器也貌似从未呈现过,那末gdb是怎样完成反向实行的呢?
为了申明这个成绩,起首我们必要回忆一些gdb的基础观点。
GDB的基础观点
Gdb的一些主要术语和gdb的全体布局
进进一个生疏的国度前先学几句他们的经常使用语会对照好。GDB这个小天下中也常常利用一些独有的名词,我们最好起首熟习他们。
Exec,指一个可实行文件,能够是ELF格局,也能够是陈旧的a.out。
Inferior,指一个正在运转的exec,一样平常就是指被调试历程。
接上去最好我们可以对gdb有一个全体的,高度归纳综合的懂得。
Gdb的计划方针是为各类平台上的人们供应一个通用的调试器,因而它必需有可扩大性,以便人们能够将它移植到分歧的硬件和软件情况下。
为了完成这个方针,GDB的系统布局接纳分层和封装的计划头脑,将那些依附于特定软硬件情况的部分举行笼统和封装。最主要的两个封装观点即是gdbarch和target。他们和gdbcore之间的干系大抵能够用下图来形貌:
.GDB的系统布局
当必要在分歧的OS上运转GDB时,只需供应响应的target即可;一样,当必要撑持一种新的新的处置器时,也只需供应新的gdbarch,而gdbcore则无需任何修正。
关于gdbarch
Gdbarch是一个封装了一切关于处置器硬件细节的数据布局。在这个数据布局中,不但包含一些形貌处置器特征的变量,也包含一些函数(喜好OO的人会天然地遐想到类。)这些函数完成了对详细硬件的一些主要操纵,好比剖析stackframe,等等。
完全的gdbarch数据布局十分复杂,没法逐一列出,下表分类总结了gdbarch中的主要信息:
表2.gdbarch数据布局
分类申明形貌硬件系统布局和ABI细节的信息好比:
endianism:年夜端体系仍是小端体系
好比return_value:形貌该处置器ABI中划定的处置函数前往值的办法
breakpoint_from_pc:用于断点交换的呆板指令,好比i386中为int3structgdbarch_tdepadditionaltargetspecificdata,beyondthatwhichis
coveredbythestandardstructgdbarch.形貌尺度数据布局的信息初级言语的int,char等尺度数据布局的详细界说会见和显现存放器的函数read_pc:前往指令指针存放器
num_regs:前往存放器的个数会见和剖析stackframe的函数分歧系统布局的stackframe都不尽不异。这些函数供应了怎样剖析和创立stackframe的详细完成函数。
能够看到gdbarch封装了一切gdb运转时所必要的硬件信息,和怎样会见和处置这些信息的详细函数。相似于面向工具计划中的类的计划,将关于处置器硬件细节的数据和办法都封装到gdbarch数据布局中。
关于target
同gdbarch一样,target也是一种封装。但target所封装的观点更庞大一些。它不但封装某一种操纵体系,也封装了分歧的”调试体例”。
起首,分歧的操纵体系对应分歧的target。一样在i386处置器下事情,Linux和vxworks关于debug的撑持是分歧的,好比怎样创立历程,怎样把持历程等。这些分歧关于gdbcore是通明的,由target来屏障。
别的,target还封装了分歧的”调试体例”。这个词对照笼统,最好是举例申明。
好比,一样是在i386Linux上面,您便可以利用native体例调试exec,也能够调试一个coredump文件,还能够attach一个正在运转的历程并调试它。翻开一个可实行文件和一个coredump文件的办法是分歧的,一样,将一个可实行文件load进内存实行和attach到一个正在实行的历程也是分歧的。
关于这些分歧,gdb也接纳target举行封装。关于gdbcore来讲,当它必要让一个调试方针入手下手运转时,便挪用target响应的回调函数,而不用体贴这个回调函数怎样完成。启动历程的详细的细节由分歧的target来详细完成。当target为exec,即一个磁盘上的可实行文件时,可使用fork+exec的体例;当target是一个远程调试方针时,能够经由过程TCP/IP发送一个命令给gdbserver举行远程调试;当target是一个已在运转的历程时,必要利用ptrace的attach命令挂载上往。诸如这些细节,gdb一切由target这个观点来封装。
这即是target这个观点的次要意义,不外,另有一些现实让target加倍庞大。
偶然候,人们但愿在统一个gdb会话中调试多个target。最多见的例子是调试coredump文件时,常常必要同时翻开发生coredume的可实行文件,以便读取标记。
好比程序a.out发生了coredump文件core.2629,当用gdb翻开coredump文件后,利用bt命令检察挪用按次时,人们不克不及看到函数名。
.没有标记信息的挪用仓库显现
此时人们常常还必要用file命令翻开a.out程序,即一个exec。
.有了标记信息的挪用仓库显现
除coredump剖析,另有别的一些情形请求gdb同时办理多个target。为了应对这些需求,gdb对target接纳了分层、优先级办理的仓库形式。仓库中的每层由一个形如xyz_stratum的乖僻名字来标示,以下图所示:
.GDB的targetstratum
这个仓库的优先级从上到下递增,gdb老是接纳最高优先级target所供应的函数举行操纵。
以,3中的命令为例,翻开coredump文件时,core_stratum层的target被push进进targetstack;当用户利用命令filea.out时,一个file_stratum层的target被push进进targetstack。他们按照本身的优先级回于分歧的层,毫不会弄错。
当targetstack中只要core_stratum的target时,假设用户但愿实行run命令是不成能的,由于coredump文件没法运转,而当载进了exectarget后,这个file_stratum层的target供应了和run命令响应的回调函数,从而使得gdb可使用run命令启动一个inferior。您在稍后的章节中能够看出,这类分层布局对反向调试的完成十分有匡助。
下表列出了target数据布局的主要内容:
表3.Target数据布局
分类申明关于target的申明信息好比:
to_name:target的名字
to_stratum:该target在targetstack中的层数把持调试方针的函数好比:
to_open:翻开target,关于exec或coredump文件实行文件翻开操纵;关于remotetarget,翻开socket创建TCP毗连等操纵.会见调试方针存放器和内存的函数好比:
to_store_registers处置断点的函数好比:
Insert_break_point把持调试历程的函数好比:
to_resume.Functiontotellthetargettostartrunningagain(orforthefirsttime).
等等。
GDB运转时的基础流程
对一个方针历程举行调试,必要操纵体系供应响应的调试功效。好比将一个正在运转的历程停息并读写其地点空间等。在传统Unix中,一样平常由ptrace体系挪用供应这些功效。本文不盘算具体先容ptrace,读者能够经由过程参考材料取得更具体的先容。
但ptrace的编程形式极年夜地影响了gdb的计划,上面我们研讨gdb怎样利用Ptrace。
起首,gdb挪用ptrace将一个方针历程停息,然后,gdb能够经由过程ptrace读写方针历程的地点空间,还能够经由过程ptrace让方针历程进进单步实行形态。Ptrace函数前往以后,gdb便入手下手守候方针历程发来的旌旗灯号,以便进一步的事情。
以单步实行为例,gdb挪用ptrace将方针历程设置为单步实行形式以后,便入手下手守候inferior的动静。由于处于单步形式,inferior每实行一条指令,Linux便将该历程挂起,并发送一个旌旗灯号给ptrace的挪用者,即gdb。Gdb承受到这条旌旗灯号(经由过程wait体系挪用)后,便晓得方针历程已完成了一次单步,然落后行响应处置,好比判别这里是不是有断点,或进进交互界面守候用户的命令等等。
这十分相似窗口体系中的动静轮回形式。Ptrace的这一事情形式影响了全部gdb的计划,不管详细的target是不是撑持ptrace,gdb都接纳这类动静轮回形式。
了解了以上的基本常识,您就能够入手下手探究反向调试的详细完成细节了。
反向调试道理和代码导读
道理概述
如前所述,反向调试的基础道理是录制回放。它将inferior运转过程当中的每条指令的实行细节录制上去,寄存到log中。当必要回退时,从log中读取前一条指令实行的细节,依据这些细节,实行undo操纵,从而将inferior恢复到事先的形态,云云便完成了“上一步”。
undo就是将某条指令所做的事情作废。好比指令A将存放器reg1的值加了1,那末undo就是将其减一。
道理很复杂,但是要将此设法付诸完成,人们必需办理几个详细的成绩:
怎样录制,又怎样回放呢?
起首,gdb7.0引进了一个新的target,叫做recordtarget。这个target供应了录制和回放的基础代码框架。
其次,当我们说到一条指令的实行细节时,事实是指那些详细内容呢?大概说我们事实应当录制些甚么呢?这些纪录怎样构造?这即是recordlist的次要内容。上面我们逐一来懂得这些常识。
Recordtarget
反向调试引进了一个新的target,叫做”record”,它事情在targetstack的第二层。Gdbtarget的分层布局带来了如许一种优点:高层的target能够复用底层target的功效,并在其上增加分外的功效。我想我们能够这么说:低层target完成基础的初级功效,高层target完成更初级的功效。
Recordtarget就是一个带录制功效的高层target,它依附低层target完成诸如启动历程,拔出断点,把持单步等基础功效。在此之上,它将inferior实行过程当中的每条指令的细节纪录上去。别的,它还处置几个反向调试独有的命令,reversenext等。
当用户但愿举行反向实行时,recordtarget其实不必要低层target的匡助。由于inferior的实行历程都已被纪录在一个log中,反向实行时,recordtarget从log中读取纪录,并undo这些纪录的影响,好比恢复先前存放器的值,恢复被指令修正的内存值等,从而到达了反向实行的效果。
上面我们具体剖析几个主要的recordtarget所供应的函数,从而对上述基础头脑有更深切的了解。
起首看record_open操纵。如前所述,Recordtarget能够看做对低层target的一个wrapper。Record_open时,起首将以后target的主要回调函数(好比后续将申明的to_resume,to_wait等)复制到一系列的beneathfunctionpointers中。然后将”recordtarget”push到targetstack的顶层。
第二个主要的操纵是record_resume。Record_resume在gdb决意让方针历程入手下手运转之前被挪用,因而这里是尽佳的录制点。该函数的完成对照复杂:
清单1.record_resume函数
record_resume (struct target_ops *ops, ptid_t ptid, int step,
enum target_signal signal)
{
record_resume_step = step;
if (!RECORD_IS_REPLAY)
{
if (do_record_message (get_current_regcache (), signal))
{
record_resume_error = 0;
}
else
{
record_resume_error = 1;
return;
}
record_beneath_to_resume (record_beneath_to_resume_ops, ptid, 1,
signal);
}
}
Record_resume起首挪用do_record_message举行录制,然后挪用低层target的to_resume函数(已保留在beneathfunctionpointer中)完成基础的resume事情。
这里必要注重一点,在挪用record_beneath_to_resume时,第三个参数step为1,即单步实行。这是由于recordtarget必要录制方针历程的每条指令,因而假设用户命令为continue或next,而不是step时,方针历程将持续实行下往直到碰到断点为止,在此时代的指令gdb没法获知,便也无从纪录。因而recordtarget强迫方针历程进进单步实行形态。以便录制每条指令。
第三个主要的操纵是record_wait。从函数的名字即可以猜得该函数是gdb守候方针历程旌旗灯号的函数。
当recordtarget实行了record_resume以后,inferior恢复实行。而gdb本人则入手下手守候inferior的旌旗灯号。后面已看到,record_resume强行让inferior进进单步形态,因而inferior在实行完一条指令后,便会被强迫挂起,并向gdb发送一个SIGCHLD旌旗灯号。此时record_wait()便入手下手实行。
该函数起首判别是不是必要举行录制,假如必要,则进一步判别以后的inferior是不是是单步实行形态,假如是,则不必要举行录制,由于即刻inferior就会停上去,而gdb再次让inferior恢复实行时将挪用record_resume,那边会实行录制事情。
但假如以后的inferior不在单步形态,且下一条指令不是断点,那末假如让inferior持续实行则意味着recordtarget将错事后续的指令实行而没法举行录制。因而,在这类情形下,record_wait将进进一个轮回,在每次轮回迭代中实行录制,并让inferior进进单步实行形态,直到碰到断点大概Inferior实行exit为止。伪代码以下:
清单2.实行录制的伪代码
while(1)
{
waitForNextSignal();
recordThisInst();
resumeInferiorAndSingleStep();
if(this is a breakpoint || this is end of inferior)
break;
}
如许,经由过程record_wait的处置,inferior的每条实行指令都将被录制上去。
Record_wait的别的一半代码是处置replay的。假设以后用户但愿反向实行,那末record_wait就从日记中读取inferior上一条实行指令的相干纪录,恢复存放器,内存等高低文,从而完成“上一步”的操纵。
Recordlist
每次实行一条指令,recordtarget便将关于该指令的信息录制上去。这些信息能够完全地形貌一条指令实行的效果。在今朝的recordtarget中,纪录的信息只包含两类:存放器和内存。
一条呆板指令可以改动的就是存放器或内存。因而每次实行一条指令,recordtarget对该指令举行剖析,假如它修正了内存,那末便纪录下被修正的内存的地点和值;假如它修正了存放器,便纪录下存放器的序号和详细的值。
这些修正纪录由structrecord_entry暗示和存储。
清单3单个纪录的数据布局
struct record_entry
{
struct record_entry *prev;
struct record_entry *next;
enum record_type type;
union
{
/* reg */
struct record_reg_entry reg;
/* mem */
struct record_mem_entry mem;
/* end */
struct record_end_entry end;
} u;
};
多个record_entry经由过程prev和next毗连成record_list链表。一条呆板指令大概既修正内存也修正存放器,因而一条指令的实行效果由record_list中的多个entry构成。有三种entry,暗示存放器的entry,暗示memory的entry和标记指令停止的entry。望文生义,registerentry用来纪录存放器的修正情形;memoryentry用来纪录内存的修正;endentry暗示指令停止。
以下图所示:
.反向调试的log布局
第一条指令inst1由三个entry构成,一个memoryentry,一个regentry和一个endentry。标明inst1修正了内存和存放器;同理,inst2,3等也利用了一样的数据布局。
函数do_record_message
函数do_record_message详细完成指令实行的录制细节。抛开gdb代码的层层挪用细节,该函数的详细事情是挪用gdbarch所供应的process_record回调函数。
关于i386,详细的process_record函数为
清单4.函数process_record界说
int i386_process_record (struct gdbarch *gdbarch, struct regcache *regcache,
CORE_ADDR addr)
这是一个1000多行的巨型函数,我倡议人人不用精读个中的每行代码。。。
大致说来,该函数起首反汇编正在实行的呆板指令,依据反汇编的了局剖析该指令是不是修正了存放器大概内存。假如有所修正,就分离分派新的regentry大概mementry并拔出到record_list,当对该指令的一切实行了局都分派了响应的record_entry以后,挪用record_arch_list_add_end拔出一个endentry,然后前往。如许,do_record_message()实行完后,关于以后指令的一切细节都被保留到record_list中了。
recordtarget实行录制的时序图
Recordtarget的录制历程用时序图来暗示对照简单了解,由于将相干操纵串起来的是事务而不是函数挪用干系。假设您盘算跟踪函数的挪用干系,那末很快就会丢失到蒙头转向。参考材料是我看到的最好的关于gdb的文档,我以为个中最棒的部分就是gdb命令实行时的时序图,这是一种十分好的暗示办法。上面我盘算用时序图来完全地形貌后面罗罗嗦嗦几千字却仍然形貌不清的器材。
最复杂的record命令序列为:
> b main
> run
> record
> continue
我们用来暗示上述命令序列所对应的、gdb外部实行历程的时序图。
.录制的时序图
当用户输出run命令后,gdb会挪用target_insert_breakpoint将今朝active的断点设置到inferior中,关于i386的arch方针,会将inferior的断点处的指令交换为int3指令。然后,gdb挪用target_resume,恢复Inferior的运转。此时,targetstack顶真个target会实行一般的target_resume操纵,好比ptrace(CONTINUE)。
当inferior实行到断点地点时,此时的指令被交换为int3,因而会发生一个trap。该旌旗灯号被gdb捕捉进进target_wait。此时gdb从头取得把持权,进进current_interp_command_loop守候用户输出命令。
接上去用户输出record命令,这将招致gdb挪用record_open函数,将recordtarget推进targetstack的栈顶,并回到gdb的命令行守候用户输出新的命令。
下一条命令是continue,gdbcore将挪用以后target的target_resume函数来恢复inferior的运转。此时的target_resume函数已酿成record_resume。经由过程后面的代码剖析,我们能够晓得,此时record_resume将挪用do_record_message完成第一条指令的录制,target_resume函数实行完将前往process()函数。Gdbcore此时将挪用target_wait()函数来守候inferior的下一个动静。一样,此时的target_wait为record_wait(),因而gdb进进record_wait()。
Record_wait将完成残剩的录制历程。由于以后的用户命令为continue,因而record_wait将进进代码清单2所示的轮回,轮回实行以下操纵:
录制以后指令
挪用下一层的resume函数并将step参数设置为一,强迫inferior进进单步实行。
守候inferior的动静
因为单步实行,inferior在实行完一条指令后又将gdb的wait操纵叫醒,持续上述轮回。云云轮回来去,直到碰到断点大概实行到exit为止,从而完成录制历程。
Recoredtarget回放功效的完成对照复杂,本文长度无限,读者能够自行剖析。
GDB反向调试的范围
Gdb的反向调试是从2006年摆布入手下手研发的,固然今朝已正式公布。但仍是不太不乱,且有一些范围。简述以下:
有sideeffect的语句固然可以回退实行,但其所酿成的sideeffect则没法打消。好比打印到屏幕上的字符其实不会由于打印语句的回退而主动消散。
因而,反向调试不合用于IO操纵。
别的,撑持反向调试的处置器系统布局还很无限,必要更多的研发职员介入出去.
停止语
良多人都在问,反向调试事实有多年夜的实践用途?我在本文的开首便复杂先容了一种利用它的场景,但我想这其实不能令心存嫌疑的人中意。实践上,以我的团体履历来看,50%的程序员历来不利用调试器。关于良多实践事情,即便不利用调试器,经由过程不休的打印和代码剖析终极也可以办理成绩。但假设能准确地利用调试器,也许可以加倍无效地办理成绩,从而将人生的可贵工夫利用在其他更成心义的中央。正如NormanMatloff所说,调试是一种艺术。没有艺术,人类仍然能够保存,但是远在1万多年前,拉科斯的史后人也要在岩洞的墙壁上涂抹出一头牛大概一匹马,那有甚么实践用途呢?
</p>
初学阶段只要把上课时候学习过的命令练熟就可以了.单靠学习各种命令而成为高手是不可能的。 通过自学老师给的资料和向同学请教,掌握了一些基本的操作,比如挂载优盘,编译程序,在Linux环境下运行,转换目录等等。学了这些基础才能进行下面的模拟OS程序。? 得到到草率的回答或者根本得不到任何Linux答案。越表现出在寻求帮助前为解决问题付出的努力,你越能得到实质性的帮助。 对于英语不是很好的读者红旗 Linux、中标Linux这些中文版本比较适合。现在一些Linux网站有一些Linux版本的免费下载,这里要说的是并不适合Linux初学者。 Linux?最大的优点在于其作为服务器的强大功能,同时支持多种应用程序及开发工具,所以Linux操作系统有着广泛的应用空间。 编程学习及开发,Linux是免费,开源的操作系统,并且可开发工具相当多,如果您支持自由软件,一定要同广大热爱自由软件人士一同为其不懈努力。 期间我阅读了不少关于Linux的相关资料,其中也不乏一些有趣的小故事,这既丰富了我的课余生活,也让我加深了对一些术语的理解,比玩游戏强多了。? 发问的时候一定要注意到某些礼节。因为Linux社区是一个松散的组织、也不承担回复每个帖子的义务。它不是技术支持。
页:
[1]