分类: LINUX
2015-01-04 09:59:10
原文地址:GDB/ARMulator 学习笔记 作者:ciuciu
1.GDB/ARMulator基本介绍
GDB/ARMulator是GDB自带的一个Armulator(ARM模拟器,实际上应该是有不止一个软件包含这个功能,例如ADS,他们都叫Armulator),不过我查看GDB的源代码都是对ARM6的模拟,而现在比较常见的是打过uclinux开发组patch的GDB/ARMulator,这样这个模拟器主要就是对Atmel AT91扳进行了模拟,而我所学习的也是这个版本,更多的信息可以到下面的URL去看。
2.GDB/ARMulator编译安装和基本使用
2.1.编译安装
如果我记得不错的话,在uclinux网站提供的arm工具链中的GDB就是打过patch的版本,你可以直接使用其。不过因为在下面要通过GDB调试的方式来帮助阅读其代码,所以在这里我决定是编译一个。
首先是在上面提到的地址下载到GDB代码以及patch,然后:
tar vxjf gdb-5.0.tar.bz2
gunzip < gdb-5.0-uclinux-armulator-20021127.patch.gz | patch -p0
cd gdb-5.0
./configure --target=arm-elf --prefix=/usr/local --without-gtk-prefix --without-gtk-exec-prefix --disable-gtktest
make
make install
2.2.基本使用
将你做好的文件系统镜象文件改名为boot.rom(这个文件和下面用的linux.2.4.x文件都可以在上面提到的地址下到),做软连接也可以,然后在这个目录中:
arm-elf-gdb linux.2.4.x
target sim
load
run
这样你就能看见uclinux在模拟器上跑起来了。
3.GDB/ARMulator代码阅读记录
GDB相关的代码很多,跟ARMulator相关的代码也不少,所以代码的阅读主要是跟着刚才跑通起uclinux使用的命令涉及的代码,其中很多地方都是用gba调试arm-elf-gdb的方式帮助阅读。
3.1.arm-elf-gdb linux.2.4.x
3.1.1.对sim的初始化
因为sim是作为gdb的一个target(应该可以称为调试目标吧 呵呵)存在的,所以在gdb运行的时候,要先对这个target进行初始化。在remote-sim.c文件中的函数_initialize_remote_sim就是对这个target的初始化函数,而这个函数将在启动的时候被调用。在这个函数中有3个操作:
第1个操作,调用init_gdbsim_ops函数,这个函数用来初始化全局结构变量gdbsim_ops,其的结构类型是target_ops,每个gdb中的target都有这么一个结构,这个结构定义在target.h文件中。这个变量的作用就是当调试开始的时候,gdb可以通过结构找到相应的对target进行控制的函数。
第2个操作,调用add_target将前面初始化好的gdbsim_ops增加到target_structs中用来方便系统对这个结构的使用。
第3个操作,用add_com给gdb增加了一个命令sim,这个命令通过调用sim_do_command这个函数可以帮助实现向sim发送命令,这个功能将在后面进行一次实际的应用,在那再详细介绍。
还有一点比较重要,sim目录下包含了很多cpu相关的目录,其中就有arm目录,这就是最基本的ARMulator的代码,gdb对ARMulator的控制就是通过target的接口函数调用ARMulator的函数来完成的。
3.1.2.对linux.2.4.x的读入
因为这个读入的操作跟使用命令file进行读入是基本一样的,所以对file命令的过程进行分析。
在用户执行file命令后,将调用exec.c文件中的函数file_command。在这个函数调用的函数exec_file_command用来将可执行文件打开,在其中target_preopen函数会检查前面是否有文件打开执行等等,并向用户提问是否进行清除,exec_file_attach则是具体的打开文件的操作,在这里打开了一个可执行文件结构exec_bfd,这将在后面被用到。symbol_file_command函数用来将符号表等进行初始化。
而在启动对可执行文件的读入在main.c文件中的函数captured_main对exec_file_command和symbol_file_command进行了调用。
3.2.target sim
在这里主要完成对sim的初始化的工作,这些工作是通过调用remote-sim.c文件中的函数gdbsim_open函数来完成的。
在开始的时候初始化了一些参数,然后传递给了wrapper.c文件中的sim_open函数,在这里设置了ARMulator的引点以及一些其他的东西。
而后调用函数push_target将gdbsim_ops增加到target_stack列表上相应的层次上,当然具体这步的意义我还不是很清晰。
最后用参数-1调用了gdbsim_fetch_register函数,当这个函数的参数为-1的时候,将以参数0到15调用这个函数,代表对arm的16个寄存器的初始化。
在这些调用的时候只做了简单的检查就调用了wrapper.c中的函数sim_fetch_register,在这个函数中第一步是调用函数init,这个函数中进行了大部分的ARMulator初始化工作。
在init函数中可以看到定义了一个static变量done,用来保证初始化只进行一次。ARMul_EmulateInit函数对ARMul_ImmedTable和ARMul_BitList,不过我没能看出这2个东西的具体作用。
ARMul_NewState函数对保存着Armulator的所有方面状态的state进行了空间分配和其中元素中的初始化,具体每个元素的定义可以看armdefs.h对ARMul_State定义中的每个元素的注释,写的很明确。在这个函数最后调用了函数ARMul_Reset,这里是对state的进一步的初始化。
在ARMul_Reset函数最开始的部分是根据state->prog32Sig也就是系统模式是否用32位模式,来设置Reg[15]和Cpsr(在26位模式中cpsr在r15中),在其后调用的函数ARMul_CPSRAltered是在修改过Cpsr后对state中其他参数值进行更新的函数。
在后面调用的mmu_reset是对state->mmu中内容进行初始化的函数。
其后是mem_reset函数,其主要作用是对state->mem也就是ARMulator中的内存进行初始化,其中还包括了将相应的文件系统内存镜象(也就是内个boot.rom)读入内存的过程,这些要做的工作都是根据全局结构数组mem_banks的值来进行的。具体是根据数组mem_banks中元素的数量来对内存进行循环的初始化,第一部是根据mem_banks元素中相应长度指定mem.rom_size中相应元素的长度并给指针mem.rom分配内存,这样就分配了内存块,如果在mem_banks的元素中filename不为空,则就将文件打开并根据big_endian进行处理后写入内存块中。这个mem_banks实际上并不单纯是在初始化的使用,在Armulator实际运行中,对内存进行读写的时候,也会根据读写的地址查找相应的其中的元素,使用这个元素中指定好的读写函数。这里提到的内存可以参看armmem.c文件。
接着被调用的函数是io_reset,这个函数初始化了state->io,这个结构的就是系统中的基本设备io设备。
最后调用了函数lcd_disable,禁止了lcd设备的工作,然后ARMul_Reset函数返回,ARMul_NewState函数也就结束了。
接着是根据big_endian设置state->bigendSig,也就是来设置ARMulator的引点。
下面调用的2个函数ARMul_MemoryInit和ARMul_OSInit都是没做什么工作,所以不做分析。
ARMul_CoProInit函数是对ARMulator中的协处理器进行了初始化。协处理器在ARMulator被处理成了ARMul_State结构中的几个函数指针CPInit、CPExit、(这2个在启动和停止的时候被调用)LDC、STC、MRC、MCR、CDP、(这几个代表了相应的协处理器指令)CPRead、CPWrite(这两个的作用还没看明白),2个数据指针CPData和CPRegWords(前面一个好象在具体代码中没用到,后面这个没搞清楚作用)组成,这样ARMulator就可以很方便的拥有扩展性很好的16个协处理器。
在ARMul_CoProInit函数中,先是循环调用ARMul_CoProDetach函数对16个CP进行了简单的函数初始化,然后给CP15进行了单独的函数初始化,CP15在arm中是跟mmu设置有关的cp,所以这里也进行的是mmu函数的初始化。接着是循环调用了每个cp的CPInit函数,然后返回。
接着在后面设置done为1表明init操作已经做过了。
后面3行我个人觉得是不必要,第一行ARMul_SelectProcessor函数实际就设置了个arm的模式,前面已经设置过了。第二行我个人认为不光是不必要,根本就是错的,设置arm为user32模式,我个人觉得一般系统启动都该是svc32启动吧,不然的话很多特权操作怎么执行。第三行就更不用说了,又执行了一次ARMul_Reset,实际上在这个函数中已经将上一行的修改覆盖掉了。所以我觉得这3行根本没什么用。
后面的部分应该是gdbsim_fetch_register函数本来该做的工作,也就是用来取得寄存器的函数,所以我觉得这个init函数放在这个函数中被执行是个有点点奇怪的过程,我是没有理解,如果我改代码的话,我会将其放到gdbsim_open去调用。
3.3.load
此命令的作用是将要调试的机器代码装入ARMulator的内存,注意前面装载的是文件系统。
首先被调用的是remote-sim.c中的函数gdbsim_load,接着就在这个函数中调用了wrapper.c中的函数sim_load。
在这个函数中第一个被调用的函数是sim-load.c中的sim_load_file,上面提到的从文件中读出机器代码然后装入ARMulator的内存的工作都是在这个函数中执行的。这里进行的工作就是先将elf文件打开,然后分析,调用函数指针do_write也就是前面传递来的函数sim_write将读出的节的内容按照其地址和长度写入ARMulator的内存。最后将打开elf文件的操作指针返回。
下面进行的是设置启动点也就是PC寄存器值的过程,因为刚才的情况,在这里启动点也就是PC还是被设置成了0。
然后函数就返回了,基本的load操作也就完成了。
3.4.run
在执行指令后被调用的是remote-sim.c中的函数gdbsim_create_inferior,在开始是对当前情况进行了一些检查和清除过去信息的工作。
接着做的是根据函数gdbsim_create_inferior的参数exec_file和args构造命令行启动参数argv,其中exec_file中是前面打开的kernel的完整路径名,而args中就是run后面跟着的执行参数,如果没有就是空字符串,整理后的多层指针中就依次在每个指针中存储了启动参数。
接着调用wrapper.c文件中的函数sim_create_inferior来对ARMulator进行近一步的初始化。
一开始又是一次对ARMulator中PC寄存器的初始化,这次因为是按照参数可执行文件句柄结构abfd也就是全局变量exec_bfd进行的。这个变量是在3.2部分介绍的过程初始化的,所以PC也是由这个结构决定的。
然后是根据argv的值对state->CommandLine进行初始化,实际就是依次将其中参数用空格分隔拷贝进去。
最后是根据env对state->MemSize进行设置。函数返回。
设置记录模拟器下层进程pid的inferior_pid为42,这么做因为在当前的情况下并没有单独的进程进行模拟,所以只进行这个设置表明模拟已经开始。
调用函数insert_breakpoints将刚才清除掉的断点重新加入代码中。用函数clear_proceed_status清楚掉以前程序执行的信息。
调用infrun.c中的函数proceed开始ARMulator的运行。这个函数的3个参数的意义分别是:
addr,运行开始的位置,如果是-1则从停止的地方也就是pc寄存器指定的位置开始执行。
siggnal,好象跟退出信号有关的一个参数,我还没搞清楚作用。
step,运行的方式,如果为非0则每执行一条指令都进入trap方便调试。
在这个函数中开始是一些初始化工作,主要是对infrun.c中函数wait_for_inferior的调用。在这个函数中对remote-sim.c文件中的wait_for_inferior进行了调用,在这个函数中调用了wrapper.c函数中的sim_resume。
sim_resume函数是Armulator启动arm指令执行的地方。在这个函数中会根据执行的参数来使用2个不同的执行函数ARMul_DoInstr()和ARMul_DoProg(),这2个函数的区别是一个单步执行而一个连续执行指令。这2个函数都是调用armemu.c中的函数ARMul_Emulate32。
ARMul_Emulate32函数就是ARMulator的核心解码和执行函数。因为各种原因,笔记就记录到这里。
4.给GDB/ARMulator增加个小功能 单步调试自动反编译
这里的目标就是给ARMulator增加一个单步自动反编译显示的功能,让ARMulator在单步的时候可以自动显示下一条要执行的汇编代码。
这个功能具体的实现可以看一下附件中带的2个修改好的文件,将这2个文件按照其所在目录拷贝进前面已经打好补丁的gdb目录中,然后编译安装就可以了。
现在我就来介绍一下我增加的代码,所有我修改的代码我都放在了
//teawater add for next_dis 2004.6.9--------------------------------------------
//AJ2D--------------------------------------------------------------------------
中。
首先是在armdefs.h的ARMul_State结构也就是表明ARMulator状态的state变量的结构增加了一个元素disassemble,用它来标明现在反编译功能是否打开的。
然后在wrapper.c的init函数中state->disassemble=0;让此功能默认为关闭的状态。
下面对wrapper.c文件中的sim_do_command函数进行修改,这个函数的作用在3.1.1提到过,可以用来是用sim命令向ARMulator发送信息,这里就用上了这个功能。参数cmd就是sim命令后面跟着的参数,通过在函数中对这个参数进行判断然后设置state->disassemble的值,通过对值的判断就可以实现对此功能的打开和关系。完成后调用 sim disassemble on 或者 sim disas on 就可以打开功能,调用 sim disassemble off 或者 sim disas off 就可以关闭功能。
修改函数sim_resume,在ARMul_DoInstr执行之后如果state->disassemble不为0就执行反编译函数,也就是显示单步执行一条指令后就反编译state->Reg[15]位置(pc寄存器,下一条要执行指令的位置)的代码。其中要注意的是对TFLAG的判断也就是对state->TFlag的判断是用来确定ARMulator当前是工作在thumb指令模式下还是arm指令模式下,并根据情况用不同的参数调用反编译函数tea_print_insn。
下面来介绍一下反编译函数tea_print_insn。单独依靠ARMul_Emulate32函数中提供的信息进行解码和显示也是可以的(以前看过objdump对i386反编译的代码,跟arm一比,arm的反编译工作确实不算多了),但并不必要,因为gdb有自己的反编译disassemble命令。
因为中间涉及的函数比较多,而且很多是static的并不可用,所以我直接介绍可用的接口函数了。这两个函数是arm-dis.c文件中的print_insn_big_arm和print_insn_big_arm,可名字就可以看出他们基本情况都是一样的,只是一个是对big endian反编译,而另一个对little endian反编译。这2个函数用的参数一样:
pc,用来标明要反编译代码的位置。
info,反编译其他相关的操作信息都存储在这里,所以在tea_print_insn函数中主要工作就是对一个disassemble_info类型结构变量的初始化。
这个结构定义在dis-asm.h中,在其中有比较详细的注释,下面介绍一下对其初始化的过程。
先将结构中全部数据初始化为0。
结构中的fprintf_func函数指针和stream配合使用来输出反编译的结果,因为fprintf_func函数指针参数的结构类似fprintf,所以我将其初始化为fprintf,而stream初始化为stdout。
endian看名字也知道是用来标记当前endian,根据state->bigendSig设置为BFD_ENDIAN_BIG或者BFD_ENDIAN_LITTLE。
read_memory_func函数指针指向的函数用来在反编译的时候被调用来取得指定位置的代码,我单独写了一个用来做这个工作的函数tea_read_memory_func,其几个参数的作用为:memaddr,指定要读取的目标地址。myaddr,存放取得到数据的地址。length,要取数据的长度。info,就是前面传递给反编译函数的这个结构。返回0表示成功非0则是出错号码。
memory_error_func函数指针指向的函数用来在当上个函数出错的时候调用这个函数来根据错误返回值来进行处理,我单独写了一个函数tea_memory_error_func。
memory_error_func函数指针指向的函数用来输出指定的地址,我让其指向tea_print_address_func函数。
symbol_at_address_func函数指针指向的函数用来确定给出的地址是否是一个符号,我用了generic_symbol_at_address函数。
disassembler_options是一个指向其他可用参数信息的,在反汇编函数中会根据这里的信息进行一些设置,我通过在其中设置force-thumb和no-force-thumb来设置反汇编函数的进行arm反编译还是thumb反编译。
其他的参数信息因为在这里没用到所以就不介绍了。
这样在使用ARMulator的时候,在调试模式下使用命令sim disassemble on打开功能,然后用stepi进行单步执行的时候,就能看见下一条要被执行的代码了。