objdump 是 gcc 套件中用来查看 ELF 文件的工具,具体详细用法不进行介绍,此处只讲解如何进行一个动态库和静态库中导出函数的查看。
1). 查看依赖项:objdump -x xxx.so | grep "NEEDED" 。下面是我查看 libsf_modbus_preproce.so 时的输出截图:
2). 查看动态符号表: objdump -T xxx.so 。假如想知道 xxx.so 中是否导出了符号 yyy ,那么命令为 objdump -T xxx.so | grep "yyy" 。下面是我查看 libsf_modbus_preproce.so 时的输出截图:
3). 查看符号表: objdump -t xxx.so 。-T 和 -t 选项在于 -T 只能查看动态符号,如库导出的函数和引用其他库的函数,而 -t 可以查看所有的符号,包括数据段的符号。下面是我查看 libsf_modbus_preproce.so 时的输出截图:
用一个反汇编调试器即可。
IDA或者OllyDbg,方便直观,当然VC也有自带的调试器,不过很麻烦,不如IDA或者OD方便。
补充:
DLL 属于可执行文件中的一类,又称为动态链接库,不能直接用DEBUG加载,一般由应用程序因使用该库中的函数,而由操作系统在应用程序加载的同时被加载入特定地址,这个地址一般是DLL在链接时指定的。当DLL被加载到运行空间,根据输出函数表,可以得到各个函数的入口地址,然后用DEBUG在各个入口下断点,调用该函数时DEBUG将跟踪进入该函数,从而实现反汇编。
反汇编属于逆向工程,逆向工程的主要手段有两大类,其中一类是动态分析,另一类是静态分析。
前面提到的方法属于动态分析,由DEBUG实现反汇编,该方法不容易得到完整的代码,一般只能形成一段一段独立分散的代码,同时由于DEBUG的局限性,反汇编的代码质量多不高,生成的代码不能直接使用,原因在于DLL在加载时若没有加载到指定地址空间,操作系统将对代码进行重定向,所以DEBUG只能得到重定向后的代码,这类代码必须修改每一个重定向点,才能形成可执行代码。作为WINDOWS32位操作系统, OLLYDBG是最为优秀的调试、跟踪、反汇编工具,多窗口运行,可以方便的通过窗口操作完成各类动作,而不需要像一般DEBUG那样由命令行来完成,OLLYDBG还有许多一般调试器不具备的功能,同时由于每一代高手不断的修改,使其具有多种功能,同时带来的就是混乱,谁也不知道有多少版本,谁也不清楚每个版本到底增加了什么功能,但就这样,也是瑕不掩疵, OLLYDBG任然是DEBUG中最强大,最好使用的。
静态分析和动态分析不同,静态分析直接打开原程序,加载而不运行,然后直接分析加载的代码。目前静态分析工具,最强大的当属IDA,IDA支持几乎所有种类的汇编语言。
IDA加载应用程序有许多选项,可以选择完整的加载整个程序,也可以选择加载程序的某个块,一般可选择的是否加载文件头、资源表、输入表、输出表等等。
IDA还支持调试,也就是说,当你在进行反汇编过程时,可以直接使用IDA来调试跟踪,以分析代码的动态执行情况,不过就动态跟踪来说,OLLYDBG更为强大。
IDA反汇编的正确率和代码的复杂程度有关,对于正规开发的代码,尤其是如果能够获得源程序的调试文件,即所谓的PDB文件,IDA可以读取PDB文件中的信息,使得反汇编的效率和准确度大为提高,生成的代码甚至比源代码易读。IDA将反汇编生成的结果存入IDB文件中。当你确认反汇编的结果达到你的要求,可以让IDA输出汇编源代码,IDA也提供其他格式的输出,例如HTML文件,便于用户阅读。楼主主要是用于分析DLL文件,一般来说这类文件更适合做静态分析,所以推荐使用IDA来进行。
IDA对于分析那些加壳或含有大量花指令、混淆代码、垃圾代码的程序,反汇编的正确率会大为下降,因为IDA无法正确的确认当期位置上的数值是属于代码,还是属于数据,是普通C字符,还是DELPHI的字符串,还是UNICODE字符串,是结构数据还是数组还是类表(DELPHI生成的代码中含有大量的类表)等等。遇到这种情况,就需要使用者掌握许多技巧,例如可以通过使用者对当前数据的认识,指导IDA如何处理当前的数据。对于大批量的,具有某些规律的数据,IDA还提供了脚本语言(文件尾位idc),通过对脚本的执行来指导IDA如何进行反汇编。对于更为复杂的情况,例如程序是自解压运行的,这时IDA就没有任何能力来进行正确的分析,通常都会用OLLYDBG动态跟踪,等程序完成自解压后从内存中将解压后的代码完整的挖下来形成文件,再由IDA进行静态分析。
对于成功进行反汇编的代码,IDA根据代码的入口、调用、转移等指令,可以为使用者提供各种格式的程序的流程图,IDA提供许多格式由用户选择,便于用户理解程序的结构。
汇编语言的科学定义,其实就是介于机器码(各种01)和高级语言(如C)之间的一种语言。你用C语言写一段程序,其实要在机器上运行的话,机器是不懂的,要经过编译器、汇编器编译,变成汇编,最终再变成机器码,机器根据这些机器码的01可以控制硬件电路完成你程序想执行的操作。
一般察看函数运行时堆栈的方法是使用GDB(bt命令)之类的外部调试器,但是,有些时候为了分析程序的BUG,(主要针对长时间运行程序的分析),在程序出错时打印出函数的调用堆栈是非常有用的。
在glibc头文件"execinfo.h"中声明了三个函数用于获取当前线程的函数调用堆栈。
[cpp] view plain copy print?。
int backtrace(void **buffer,int size) 。
该函数用于获取当前线程的调用堆栈,获取的信息将会被存放在buffer中,它是一个指针列表。参数 size 用来指定buffer中可以保存多少个void* 元素。函数返回值是实际获取的指针个数,最大不超过size大小。
在buffer中的指针实际是从堆栈中获取的返回地址,每一个堆栈框架有一个返回地址。
注意:某些编译器的优化选项对获取正确的调用堆栈有干扰,另外内联函数没有堆栈框架;删除框架指针也会导致无法正确解析堆栈内容。
[cpp] view plain copy print?。
char ** backtrace_symbols (void *const *buffer, int size) 。
backtrace_symbols将从backtrace函数获取的信息转化为一个字符串数组. 参数buffer应该是从backtrace函数获取的指针数组,size是该数组中的元素个数(backtrace的返回值) 。
函数返回值是一个指向字符串数组的指针,它的大小同buffer相同.每个字符串包含了一个相对于buffer中对应元素的可打印信息.它包括函数名,函数的偏移地址,和实际的返回地址。
现在,只有使用ELF二进制格式的程序才能获取函数名称和偏移地址.在其他系统,只有16进制的返回地址能被获取.另外,你可能需要传递相应的符号给链接器,以能支持函数名功能(比如,在使用GNU ld链接器的系统中,你需要传递(-rdynamic), -rdynamic可用来通知链接器将所有符号添加到动态符号表中,如果你的链接器支持-rdynamic的话,建议将其加上!)。
该函数的返回值是通过malloc函数申请的空间,因此调用者必须使用free函数来释放指针.。
注意:如果不能为字符串获取足够的空间函数的返回值将会为NULL。
[cpp] view plain copy print?。
void backtrace_symbols_fd (void *const *buffer, int size, int fd) 。
backtrace_symbols_fd与backtrace_symbols 函数具有相同的功能,不同的是它不会给调用者返回字符串数组,而是将结果写入文件描述符为fd的文件中,每个函数对应一行.它不需要调用malloc函数,因此适用于有可能调用该函数会失败的情况。
下面是glibc中的实例(稍有修改):
[cpp] view plain copy print?。
#include <execinfo.h> 。
#include <stdio.h> 。
#include <stdlib.h> 。
/* Obtain a backtrace and print it to @code{stdout}. */ 。
void print_trace (void) 。
{
void *array[10]; 。
size_t size; 。
char **strings; 。
size_t i; 。
size = backtrace (array, 10); 。
strings = backtrace_symbols (array, size); 。
if (NULL == strings) 。
{
perror("backtrace_synbols"); 。
Exit(EXIT_FAILURE); 。
}
printf ("Obtained %zd stack frames.\n", size); 。
for (i = 0; i < size; i++) 。
printf ("%s\n", strings[i]); 。
free (strings); 。
strings = NULL; 。
}
/* A dummy function to make the backtrace more interesting. */ 。
void dummy_function (void) 。
{
print_trace (); 。
}
int main (int argc, char *argv[]) 。
{
dummy_function (); 。
return 0; 。
}
输出如下:
[cpp] view plain copy print?。
Obtained 4 stack frames. 。
./execinfo() [0x80484dd] 。
./execinfo() [0x8048549] 。
./execinfo() [0x8048556] 。
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3) [0x70a113] 。
我们还可以利用这backtrace来定位段错误位置。
通常情况系,程序发生段错误时系统会发送SIGSEGV信号给程序,缺省处理是退出函数。我们可以使用 signal(SIGSEGV, &your_function);函数来接管SIGSEGV信号的处理,程序在发生段错误后,自动调用我们准备好的函数,从而在那个函数里来获取当前函数调用栈。
举例如下:
[cpp] view plain copy print?。
#include <stdio.h> 。
#include <stdlib.h> 。
#include <stddef.h> 。
#include <execinfo.h> 。
#include <signal.h> 。
void dump(int signo) 。
{
void *buffer[30] = {0}; 。
size_t size; 。
char **strings = NULL; 。
size_t i = 0; 。
size = backtrace(buffer, 30); 。
fprintf(stdout, "Obtained %zd stack frames.nm\n", size); 。
strings = backtrace_symbols(buffer, size); 。
if (strings == NULL) 。
{
perror("backtrace_symbols."); 。
exit(EXIT_FAILURE); 。
}
for (i = 0; i < size; i++) 。
{
fprintf(stdout, "%s\n", strings[i]); 。
}
free(strings); 。
strings = NULL; 。
exit(0);
}
void func_c() 。
{
*((volatile char *)0x0) = 0x9999; 。
}
void func_b() 。
{
func_c(); 。
}
void func_a() 。
{
func_b(); 。
}
int main(int argc, const char *argv[]) 。
{
if (signal(SIGSEGV, dump) == SIG_ERR) 。
perror("can't catch SIGSEGV"); 。
func_a(); 。
return 0; 。
}
编译程序:
gcc -g -rdynamic test.c -o test; ./test。
输出如下:
[cpp] view plain copy print?。
Obtained6stackframes.nm 。
./backstrace_debug(dump+0x45)[0x80487c9] 。
[0x468400]
./backstrace_debug(func_b+0x8)[0x804888c] 。
./backstrace_debug(func_a+0x8)[0x8048896] 。
./backstrace_debug(main+0x33)[0x80488cb] 。
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf3)[0x129113] 。
(这里有个疑问: 多次运行的结果是/lib/i368-Linux-gnu/libc.so.6和[0x468400]的返回地址是变化的,但不变的是后三位, 不知道为什么)
接着:
objdump -d test > test.s。
在test.s中搜索804888c如下:
[cpp] view plain copy print?。
8048884 <func_b>: 。
8048884: 55 push %ebp 。
8048885: 89 e5 mov %esp, %ebp 。
8048887: e8 eb ff ff ff call 8048877 <func_c> 。
804888c: 5d pop %ebp 。
804888d: c3 ret 。
其中80488c时调用(call 8048877)C函数后的地址,虽然并没有直接定位到C函数,通过汇编代码, 基本可以推出是C函数出问题了(pop指令不会导致段错误的)。
我们也可以通过addr2line来查看。
[cpp] view plain copy print?。
addr2line 0x804888c -e backstrace_debug -f 。
输出:
[cpp] view plain copy print?。
func_b
/home/astrol/c/backstrace_debug.c:57 。
以下是简单的backtrace原理实现:
驱动程序开发的一个重大难点就是不易调试。本文目的就是介绍驱动开发中常用的几种直接和间接的调试手段,它们是:
1、利用printk
2、查看OOP消息
3、利用strace
4、利用内核内置的hacking选项。
5、利用ioctl方法
6、利用/proc 文件系统
7、使用kgdb
前两种如下:
一、利用printk
这是驱动开发中最朴实无华,同时也是最常用和有效的手段。scull驱动的main.c第338行如下,就是使用printk进行调试的例子,这样的例子相信大家在阅读驱动源码时随处可见。
338 // printk(KERN_ALERT "wakeup by signal in process %d\n", current->pid);。
printk的功能与我们经常在应用程序中使用的printf是一样的,不同之处在于printk可以在打印字符串前面加上内核定义的宏,例如上面例子中的KERN_ALERT(注意:宏与字符串之间没有逗号)。
#define KERN_EMERG "<0>"。
#define KERN_ALERT "<1>"。
#define KERN_CRIT "<2>"。
#define KERN_ERR "<3>"。
#define KERN_WARNING "<4>"。
#define KERN_NOTICE "<5>"。
#define KERN_INFO "<6>"。
#define KERN_DEBUG "<7>"。
#define DEFAULT_CONSOLE_LOGLEVEL 7。
这个宏是用来定义需要打印的字符串的级别。值越小,级别越高。内核中有个参数用来控制是否将printk打印的字符串输出到控制台(屏幕或者/sys/log/syslog日志文件)
# cat /proc/sys/kernel/printk。
6 4 1 7。
第一个6表示级别高于(小于)6的消息才会被输出到控制台,第二个4表示如果调用printk时没有指定消息级别(宏)则消息的级别为4,第三个1表示接受的最高(最小)级别是1,第四个7表示系统启动时第一个6原来的初值是7。
因此,如果你发现在控制台上看不到你程序中某些printk的输出,请使用echo 8 > /proc/sys/kernel/printk来解决。
在复杂驱动的开发过程中,为了调试会在源码中加入成百上千的printk语句。而当调试完毕形成最终产品的时候必然会将这些printk语句删除想想驱动的使用者而不是开发者吧。记住:己所不欲,勿施于人),这个工作量是不小的。最要命的是,如果我们将调试用的printk语句删除后,用户又报告驱动有bug,所以我们又不得不手工将这些上千条的printk语句再重新加上。oh,my god,杀了我吧。所以,我们需要一种能方便地打开和关闭调试信息的手段。哪里能找到这种手段呢?哈哈,远在天边,近在眼前。看看scull驱动或者leds驱动的源代码吧!
#define LEDS_DEBUG。
#undef PDEBUG 。
#ifdef LEDS_DEBUG。
#ifdef __KERNEL__。
#define PDEBUG(fmt, args…) printk( KERN_EMERG "leds: " fmt, ## args)。
#else
#define PDEBUG(fmt, args…) fprintf(stderr, fmt, ## args)。
#endif
#else
#define PDEBUG(fmt, args…)。
#endif
#undef PDEBUGG
#define PDEBUGG(fmt, args…)。
这样一来,在开发驱动的过程中,如果想打印调试消息,我们就可以用PDEBUG("address of i_cdev is %p\n", inode->i_cdev);,如果不想看到该调试消息,就只需要简单的将PDEBUG改为PDEBUGG即可。而当我们调试完毕形成最终产品时,只需要简单地将第1行注释掉即可。
上边那一段代码中的__KERNEL__是内核中定义的宏,当我们编译内核(包括模块)时,它会被定义。当然如果你不明白代码中的…和##是什么意思的话,就请认真查阅一下gcc关于预处理部分的资料吧!如果你实在太懒不愿意去查阅的话,那就充当VC工程师把上面的代码copy到你的代码中去吧。
二、查看OOP消息
OOP意为惊讶。当你的驱动有问题,内核不惊讶才怪:嘿!小子,你干吗乱来!好吧,就让我们来看看内核是如何惊讶的。
根据faulty.c(单击下载)编译出faulty.ko,并 insmod faulty.ko。执行echo yang >/dev/faulty,结果内核就惊讶了。内核为什么会惊讶呢?因为faulty驱动的write函数执行了*(int *)0 = 0,向内存0地址写入,这是内核绝对不会容许的。
52 ssize_t faulty_write (struct file *filp, const char __user *buf, size_t count,。
53 loff_t *pos)。
54 {
55
56 *(int *)0 = 0;。
57 return 0;。
58 }
1 Unable to handle kernel NULL pointer dereference at virtual address 00000000。
2 pgd = c3894000。
3 [00000000] *pgd=33830031, *pte=00000000, *ppte=00000000。
4 Internal error: Oops: 817 [#1] PREEMPT。
5 Modules linked in: faulty scull。
6 CPU: 0 Not tainted (2.6.22.6 #4)。
7 PC is at faulty_write+0×10/0×18 [faulty]。
8 LR is at vfs_write+0xc4/0×148。
9 pc : [] lr : [] psr: a0000013。
10 sp : c3871f44 ip : c3871f54 fp : c3871f50。
11 r10: 4021765c r9 : c3870000 r8 : 00000000。
12 r7 : 00000004 r6 : c3871f78 r5 : 40016000 r4 : c38e5160。
13 r3 : c3871f78 r2 : 00000004 r1 : 40016000 r0 : 00000000。
14 Flags: NzCv IRQs on FIQs on Mode SVC_32 Segment user。
15 Control: c000717f Table: 33894000 DAC: 00000015。
16 Process sh (pid: 745, stack limit = 0xc3870258)。
17 Stack: (0xc3871f44 to 0xc3872000)。
18 1f40: c3871f74 c3871f54 c0088eb8 bf00608c 00000004 c38e5180 c38e5160。
19 1f60: c3871f78 00000000 c3871fa4 c3871f78 c0088ffc c0088e04 00000000 00000000。
20 1f80: 00000000 00000004 40016000 40215730 00000004 c002c0e4 00000000 c3871fa8。
21 1fa0: c002bf40 c0088fc0 00000004 40016000 00000001 40016000 00000004 00000000。
22 1fc0: 00000004 40016000 40215730 00000004 00000001 00000000 4021765c 00000000。
23 1fe0: 00000000 bea60964 0000266c 401adb40 60000010 00000001 00000000 00000000。
24 Backtrace:
25 [] (faulty_write+0×0/0×18 [faulty]) from [] (vfs_write+0xc4/0×148)。
26 [] (vfs_write+0×0/0×148) from [] (sys_write+0x4c/0×74)。
27 r7:00000000 r6:c3871f78 r5:c38e5160 r4:c38e5180。
28 [] (sys_write+0×0/0×74) from [] (ret_fast_syscall+0×0/0x2c)。
29 r8:c002c0e4 r7:00000004 r6:40215730 r5:40016000 r4:00000004。
30 Code: e1a0c00d e92dd800 e24cb004 e3a00000 (e5800000)。
1行惊讶的原因,也就是报告出错的原因;
2-4行是OOP信息序号;
5行是出错时内核已加载模块;
6行是发生错误的CPU序号;
7-15行是发生错误的位置,以及当时CPU各个寄存器的值,这最有利于我们找出问题所在地;
16行是当前进程的名字及进程ID。
17-23行是出错时,栈内的内容。
24-29行是栈回溯信息,可看出直到出错时的函数递进调用关系(确保CONFIG_FRAME_POINTER被定义)
30行是出错指令及其附近指令的机器码,出错指令本身在小括号中。
反汇编faulty.ko( arm-linux-objdump -D faulty.ko > faulty.dis ;cat faulty.dis)可以看到如下的语句如下:
0000007c :
7c: e1a0c00d mov ip, sp。
80: e92dd800 stmdb sp!, {fp, ip, lr, pc}。
84: e24cb004 sub fp, ip, #4 ; 0×4。
88: e3a00000 mov r0, #0 ; 0×0。
8c: e5800000 str r0, [r0]。
90: e89da800 ldmia sp, {fp, sp, pc}。
定位出错位置以及获取相关信息的过程:
9 pc : [] lr : [] psr: a0000013。
25 [] (faulty_write+0×0/0×18 [faulty]) from [] (vfs_write+0xc4/0×148)。
26 [] (vfs_write+0×0/0×148) from [] (sys_write+0x4c/0×74)。
出错代码是faulty_write函数中的第5条指令((0xbf00608c-0xbf00607c)/4+1=5),该函数的首地址是0xbf00607c,该函数总共6条指令(0×18),该函数是被0xc0088eb8的前一条指令调用的(即:函数返回地址是0xc0088eb8。这一点可以从出错时lr的值正好等于0xc0088eb8得到印证)。调用该函数的指令是vfs_write的第49条(0xc4/4=49)指令。
达到出错处的函数调用流程是:write(用户空间的系统调用)–>sys_write–>vfs_write–>faulty_write。
OOP消息不仅让我定位了出错的地方,更让我惊喜的是,它让我知道了一些秘密:1、gcc中fp到底有何用处?2、为什么gcc编译任何函数的时候,总是要把3条看上去傻傻的指令放在整个函数的最开始?3、内核和gdb是如何知道函数调用栈顺序,并使用函数的名字而不是地址? 4、我如何才能知道各个函数入栈的内容?哈哈,我渐渐喜欢上了让内核惊讶,那就再看一次内核惊讶吧。
执行 cat /dev/faulty,内核又再一次惊讶!
1 Unable to handle kernel NULL pointer dereference at virtual address 0000000b。
2 pgd = c3a88000。
3 [0000000b] *pgd=33a79031, *pte=00000000, *ppte=00000000。
4 Internal error: Oops: 13 [#2] PREEMPT。
5 Modules linked in: faulty。
6 CPU: 0 Not tainted (2.6.22.6 #4)。
7 PC is at vfs_read+0xe0/0×140。
8 LR is at 0xffffffff。
9 pc : [] lr : [] psr: 20000013。
10 sp : c38d9f54 ip : 0000001c fp : ffffffff。
11 r10: 00000001 r9 : c38d8000 r8 : 00000000。
12 r7 : 00000004 r6 : ffffffff r5 : ffffffff r4 : ffffffff。
13 r3 : ffffffff r2 : 00000000 r1 : c38d9f38 r0 : 00000004。
14 Flags: nzCv IRQs on FIQs on Mode SVC_32 Segment user。
15 Control: c000717f Table: 33a88000 DAC: 00000015。
16 Process cat (pid: 767, stack limit = 0xc38d8258)。
17 Stack: (0xc38d9f54 to 0xc38da000)。
18 9f40: 00002000 c3c105a0 c3c10580。
19 9f60: c38d9f78 00000000 c38d9fa4 c38d9f78 c0088f88 c0088bb4 00000000 00000000。
20 9f80: 00000000 00002000 bef07c80 00000003 00000003 c002c0e4 00000000 c38d9fa8。
21 9fa0: c002bf40 c0088f4c 00002000 bef07c80 00000003 bef07c80 00002000 00000000。
22 9fc0: 00002000 bef07c80 00000003 00000000 00000000 00000001 00000001 00000003。
23 9fe0: 00000000 bef07c6c 0000266c 401adab0 60000010 00000003 00000000 00000000。
24 Backtrace: invalid frame pointer 0xffffffff。
25 Code: ebffff86 e3500000 e1a07000 da000015 (e594500c)。
26 Segmentation fault。
不过这次惊讶却令人大为不解。OOP竟然说出错的地方在vfs_read(要知道它可是大拿们千锤百炼的内核代码),这怎么可能?哈哈,万能的内核也不能追踪函数调用栈了,这是为什么?其实问题出在faulty_read的43行,它导致入栈的r4、r5、r6、fp全部变为了0xffffffff,ip、lr的值未变,这样一来faulty_read函数能够成功返回到它的调用者——vfs_read。但是可怜的vfs_read(忠实的APTCS规则遵守者)并不知道它的r4、r5、r6已经被万恶的faulty_read改变,这样下去vfs_read命运就可想而知了——必死无疑!虽然内核很有能力,但缺少了正确的fp的帮助,它也无法追踪函数调用栈。
36 ssize_t faulty_read(struct file *filp, char __user *buf,。
37 size_t count, loff_t *pos)。
38 {
39 int ret;。
40 char stack_buf[4];。
41
42
43 memset(stack_buf, 0xff, 20);。
44 if (count > 4)。
45 count = 4;。
46 ret = copy_to_user(buf, stack_buf, count);。
47 if (!ret)。
48 return count;。
49 return ret;。
50 }
00000000 :
0: e1a0c00d mov ip, sp。
4: e92dd870 stmdb sp!, {r4, r5, r6, fp, ip, lr, pc}。
8: e24cb004 sub fp, ip, #4 ; 0×4。
c: e24dd004 sub sp, sp, #4 ; 0×4,这里为stack_buf[]在栈上分配1个字的空间,局部变量ret使用寄存器存储,因此就不在栈上分配空间了。
10: e24b501c sub r5, fp, #28 ; 0x1c。
14: e1a04001 mov r4, r1。
18: e1a06002 mov r6, r2。
1c: e3a010ff mov r1, #255 ; 0xff。
20: e3a02014 mov r2, #20 ; 0×14。
24: e1a00005 mov r0, r5。
28: ebfffffe bl 28 //这里在调用memset。
78: e89da878 ldmia sp, {r3, r4, r5, r6, fp, sp, pc}。
这次OOP,深刻地认识到:
内核能力超强,但它不是,也不可能是万能的。所以即使你能力再强,也要和你的team member搞好关系,否则在关键时候你会倒霉的;
出错的是faulty_read,vfs_read却做了替罪羊。所以人不要被表面现象所迷惑,要深入看本质;
内核本来超级健壮,可是你写的驱动是内核的组成部分,由于它出错,结果整体崩盘。所以当你加入一个团队的时候一定要告诫自己,虽然你的角色也许并不重要,但你的疏忽大意将足以令整个非常牛X的团队崩盘。反过来说,当你是team leader的时候,在选团队成员的时候一定要慎重、慎重、再慎重,即使他只是一个小角色。
千万别惹堆栈,它一旦出问题,定位错误将会是一件非常困难的事情。所以,千万别惹你的领导,否则将死得很难看。
不管是初步跨入Linux殿堂的新手,还是具有多年经验的专家,在安装或编译软件包的过程中或多或少的都会遇到包的依赖问题,从而导致安装过程无法继续,比如管理员在安装LAMP时,包需要libgd.so文件,而这个文件属于GD软件包。但是在安装GD软件包时,可能这个软件包跟其他软件包又具有依赖关系,又需要安装其他软件包才行。这时有的管理员便失去耐心。在遇到这种Linux软件包依赖关系问题时,该如何解决呢?在谈这个具体的措施之前,先跟大家聊聊Linux系统里的软件依赖性问题。
一、什么是依赖性
程序依赖于程序代码的共享库,以便它们可以发出系统调用将输出发送到设备或打开文件等(共享库存在于许多方面,而不只局限于系统调用)。没有共享库,每次程序员开发一个新的程序,每个程序员都需要从头开始重写这些基本的系统操作。当编译程序时,程序员将他的代码链接到这些库。如果链接是静态的,编译后的共享库对象代码就添加到程序执行文件中;如果是动态的,编译后的共享库对象代码只在运行时需要它时由程序员加载。动态可执行文件依赖于正确的共享库或共享对象来进行操作。rpm依赖性尝试在安装时强制实施动态可执行文件的共享对象需求,以便在以后当程序运行时不会有与动态链接过程有关的任何问题。
注意:还有一种类型的依赖性,它基于显式的条目,rpm通过程序员将该依赖性强加到rpm配置文件中,但目前我们不关心这种类型的依赖性,这种依赖性比较容易解决。这里将重点放在rpm强制实施的更加复杂的共享对象依赖性。
二、动态可执行文件和共享对象
动态可执行文件使用最初编译和链接程序时使用的库文件的共享对象名称来查找共享对象。它们在少数的几个标准位置查找,比如在/lib和/usr/lib目录及在LD_LIBRARY_PATH环境变量(主要用于指定查找共享库,比如我们在安装Oracle时指定路径,exportLD_LIBRARY_PATH=$ORACLE_HOME/lib:/lib:/usr/lib:/usr/local/lib)指定的目录中。顺便提一下,在这些库目录中找到的共享对象可能不是真正的文件;它们可能是指向位于其他位置的真实库文件的符号链接(但通常仍旧在标准库目录的一个目录中)。至少从系统管理员的观点是在用于创建共享库文件的共享库软件包的名称和共享库文件的名称之间通常没有什么关系。例如,GLIBC2.3软件包用于创建libc.so.6共享库文件。也从本示例中注意到,添加到共享库文件名结束的版本号(.6)跟用于创建它的版本号(2.3)没有关系。这是由共享库软件包开发人员有意完成的,以便GLIBC的新版本可以重用相同的共享库文件名libc.so.6。这允许您在系统上加载新版本的GLIBC,而不用中断动态链接到lib.so.6共享库文件的所有程序,当然假定新版本的GLIBC向后与动态可执行文件最初所链接的老版本GLIBC兼容。因此,即使库文件或共享对象文件有与它们相关的版本号,这些版本号也不能帮助你确定他们来自哪个版本的共享软件包。
注意:当将whatprovides选项用于rpm查询命令时,可以获得有关使用rpm软件包加载到系统的现有共享对象的信息。这种混乱是由下面的事实造成的:单个共享库文件可能支持某个范围的共享库软件包版本。例如,要检查soname库文件/lib/libc.so.6支持的GLIBC共享库软件包,运行下面的命令:
#objdump--all-headers/lib/libc.so.6|less。
向下滚动此报告,直到到达Versiondefinitions:部分,以便查看libc.so.6共享库文件支持哪些GLIBC版本:
Versiondefinitions:。
10x010x0865f4e6libc.so.6。
20x000x0d696910GLIBC_2.0。
30x000x0d696911GLIBC_2.1。
GLIBC_2.0
40x000x09691f71GLIBC_2.1.1。
GLIBC_2.1
50x000x09691f72GLIBC_2.1.2。
GLIBC_2.1.1
60x000x09691f73GLIBC_2.1.3。
GLIBC_2.1.2
70x000x0d696912GLIBC_2.2。
GLIBC_2.1.3
80x000x09691a71GLIBC_2.2.1。
GLIBC_2.2
90x000x09691a72GLIBC_2.2.2。
GLIBC_2.2.1
100x000x09691a73GLIBC_2.2.3。
GLIBC_2.2.2
110x000x09691a74GLIBC_2.2.4。
GLIBC_2.2.3
120x000x09691a76GLIBC_2.2.6。
GLIBC_2.2.4
130x000x0d696913GLIBC_2.3。
GLIBC_2.2.6
140x000x09691972GLIBC_2.3.2。
GLIBC_2.3
150x000x09691973GLIBC_2.3.3。
GLIBC_2.3.2
160x000x09691974GLIBC_2.3.4。
GLIBC_2.3.3
170x000x0d696914GLIBC_2.4。
GLIBC_2.3.4
180x000x0d696915GLIBC_2.5。
GLIBC_2.4
190x000x0963cf85GLIBC_PRIVATE。
GLIBC_2.5
200x000x0b792650GCC_3.0。
在本示例中,1ibc.so.6共享库文件支持原先为GLIBC版本2.0到2.5而开发的所有动态执行文件。注意:也可以使用objdump命令来从共享库文件中提取soname,命令如下所示:
#objdump--all-headers/lib/libcrypto.so.0.9.8b|grepSONAME。
SONAMElibcrypto.so.6。
objdump:/lib/libcrypto.so.0.9.8b:norecognizeddebugginginformation。
接下来,将讨论rpm软件包是如何生成的,以便在新系统上安装rpm软件包时,这些共库依赖性是己知的。
三、Rpm软件包和共享库依赖性。
当程序员生成rpm软件包时,ldd命令用于报告动态可执行文件软件包中所有动态可执行文件使用的所有共享库。另一个混乱是由下面的事实带来的:相同软件包中的不同动态可执行文件可能与相同的共享库软件包的不同版本进行链接。例如,Heartbeat软件包中的不同程序可能已经进行了开发,并动态链接到libc.so.6sonmae共享库文件的不同GLIBC版本。对rpm命令使用-q和--requires参数,可以看到rpm软件包需要的共享库的完整清单。例如,要看到Heartbeatrpm软件包所有的所需依赖性,请使用命令:
#rpm-q--requires-pheartbeat-1.x.x.i386.rpm。
这产生了下面的报告:
sysklogd
/bin/sh
/bin/sh
/usr/bin/python。
ld-linux.so.2
libapphb.so.0
libc.so.6
libc.so.6(GLIBC_2.0)。
libc.so.6(GLIBC_2.1)。
libc.so.6(GLIBC_2.1.3)。
libc.so.6(GLIBC_2.2)。
libc.so.6(GLIBC_2.3)。
libccmclient.so.0。
libdl.so.2
libglib-1.2.so.0。
libhbclient.so.0。
libpils.so.0
libplumb.so.0
libpthread.so.0。
librt.so.1
libstonith.so.0。
注意,在此报告中,libc.so.6soname是所需要的,此共享库必须支持使用GLIBC共享软件包版本号2.0、2.1、2.1.3、2.2和2.3进行链接的动态可执行文件。这是由下面的事实决定的:Heartbeat软件包中的不同动态可执行文件是针对不同版本的libc.so.6库的每个版本进行链接的。在了解了动态可执行文件、共享对象、soname和共享库软件包彼此是如何相关的后,下面准备来看这样的一个例子:当尝试安装rpm软件包,并且它由于依赖性错误而失败时,会发生什么。yum能够从指定的服务器自动下载RPM包并且安装,可以自动处理依赖性关系,并且一次安装所有依赖的软体包,无须繁琐地一次次下载、安装。
四、手工解决依赖性问题
通常,当尝试安装发行版中没有包括的软件包(及不能由像up2date、apt-get或Yum一样的更新工具自动解决其依赖性的软件包)时,将碰到rpm依赖性错误。例如,如果尝试在老的Linux发行版上使用rpm–ivh*rpm命令,例如所有的Heartbeatrpm包,那么在安装过程中就可能碰到下面的错误:
error:faileddependencies:。
libc.so.6(GLIBC_2.3)isneededbyheartbeat-1.x.x。
libc.so.6(GLIBC_2.3)isneededbyheartbeat-pils-1.x.x。
libcrypto.so.0.9.6isneededbyheartbeat-stonith-1.x.x。
libsnmp-0.4.2.6.soisneededbyheartbeat-stonith-1.x.x。
注意,rpm命令没有干扰报告所需的每个GLIBC共享库软件包版本号——它只报告所需的最高编号的版本号(GLIBC_2.3)。(假定原来的软件包开发人员不会将相同软件包中的可执行文件链接到不兼容版本的共享库软件包)所有的这些故障都报告所需的共享库名称或soname(而不是文件名称,soname始终以“lib”开始)。但可以删除添加到rpm报告的soname结束的版本号,并快速检查以查看是否在系统中使用locate命令安装这些共享库(假设您的locate数据库是最新的,有关更多信息,请参阅locate或slocate的手册页)。例如,要查找libcrypto享库文件,要输入:
#locatelibcrypto。
[root@localhost~]#locatelibcrypto。
/lib/libcrypto.so.0.9.8b。
/lib/libcrypto.so.6。
/root/.Trash/vmware-tools-distrib/lib/lib32/libcrypto.so.0.9.8。
/root/.Trash/vmware-tools-distrib/lib/lib32/libcrypto.so.0.9.8/libcrypto.so.0.9.8。
/root/.Trash/vmware-tools-distrib/lib/lib64/libcrypto.so.0.9.8。
/root/.Trash/vmware-tools-distrib/lib/lib64/libcrypto.so.0.9.8/libcrypto.so.0.9.8。
/usr/lib/libcrypto.a。
/usr/lib/libcrypto.so。
/usr/lib/pkgconfig/libcrypto.pc。
/usr/lib/vmware-tools/lib32/libcrypto.so.0.9.8。
/usr/lib/vmware-tools/lib32/libcrypto.so.0.9.8/libcrypto.so.0.9.8。
/usr/lib/vmware-tools/lib64/libcrypto.so.0.9.8。
/usr/lib/vmware-tools/lib64/libcrypto.so.0.9.8/libcrypto.so.0.9.8。
如果此命令没有在系统上找到一个libcrypto共享库文件,将需要转到Internet并找出哪个共享库软件包包含此共享库文件。完成此项工具的一个快速和简便方式是只要在http://rpmfind.net上将共享库的名称输入到搜索栏中。如果将文本libcrypto.so输入到此搜索贞中,将很快知道此共享库是由openssl软件包提供的。
如果老版本的共享库数据包已经安装在系统上,可以用如下的命令确认此软件包含您需要的共享库文件:
#rpm-q--providesopenssl。
[root@localhost~]#rpm-q--providesopenssl。
config(openssl)=0.9.8b-10.el5。
lib4758cca.so
libaep.so
libatalla.so
libchil.so
libcrypto.so.6
libcswift.so
libgmp.so
libnuron.so
libssl.so.6
libsureware.so
libubsec.so
openssl=0.9.8b-10.el5。
此命令报告此rpm软件包中提供的所有内容(这包括软件包提供的共享库文件的soname)。注意:如前面指出的,共享库软件包版本号没有并且应该没有与共享库文件(soname)版本号的任何对应关系。这里不进行这方面的讨论,因为soname符号链接可能指向不同版本的共享库文件,这也是在尽量避免在安装新版本的共享软件包时中断现有动态可执行文件的情况下完成的。
五、自动解决依赖性故障
当您使用rpm软件包来生成、升级或添加新的特性到系统时,依赖性故障可能很快变成一场恶梦。只要通过使用您的发行版供应商的升级服务或工具,就可以避免这场恶梦。例如,当选择要安装的rpm软件包时,RedHat工具up2date自动从RedHat下载并安装所有rpm依赖性。下面就点上列出了几个完成相同事情的支持社区的免费方法:http://www.rpm.org/。下面将只进一步看到这些自动更新工具中的一种:Yum。
1.使用Yum来安装rpm软件包。
Yum(YellowdogUpdater,Modified)程序可从下面网址下载:http://yum.baseurl.org/download/3.4/yum-3.4.3.tar.gz。
在下载了此软件包后,可以使用下面的命令像任何其他rpm软件包那样安装它:
#rpm-ivhyum*
您可能需要更新想用于下载您的rpm软件包的存储库。有关Fedora的可用Yum存储库的清单在http://www.fedoratracker.org要切换到不同的存储库,下载这些文件中的一个文件,并将该文件作为/etc/yum.conf文件安装。现在可以用下面的命令告诉Yum报告存储在Yum存储库中、可用于安装所有软件包:
#yumlist
[root@localhost~]#yumlist|more。
ThissystemisnotregisteredwithRHN.。
RHNsupportwillbedisabled.。
Loading"security"plugin。
Loading"rhnplugin"plugin。
InstalledPackages。
Deployment_Guide-en-US.noarch5.2-9installed。
Deployment_Guide-zh-CN.noarch5.2-9installed。
Deployment_Guide-zh-TW.noarch5.2-9installed。
GConf2.i3862.14.0-9.el5installed。
GConf2-devel.i3862.14.0-9.el5installed。
ImageMagick.i3866.2.8.0-4.el5_1.1installed。
MAKEDEV.i3863.23-1.2installed。
MySQL-python.i3861.2.1-1installed。
NetworkManager.i3861:0.6.4-8.el5installed。
NetworkManager-glib.i3861:0.6.4-8.el5installed。
2.用Yum安装新的rpm软件包。
在本示例中,将安装新的GLIBC软件包。用简单的命令安装最新的GLIBC及其所有依赖性:
#yumupdateglibc。
如果一切正常,Yum程序将自动检测、下载并安装最新GLIBC软件包所需要的所有rpm软件包(这里的GLIBC软件包是为您的发行版而构建的,不一定是可用的最新版GLIBC软件包(使用发行版所批准的GLIBC共享库软件包版本号或冒险安装没有使用正常系统操作所需要的动态可执行文件的GLIBC软件包版本)。也可以将list参数用于Yum和grep命令来查找要安装的软件包。例如,要查找名称中有SNMP的软件包,请输入:
#yumlist|grepsnmp。
此命令返回如下报告:
ThissystemisnotregisteredwithRHN.。
RHNsupportwillbedisabled.。
net-snmp.i3861:5.3.1-24.el5installed。
net-snmp-libs.i3861:5.3.1-24.el5installed。
net-snmp-perl.i3861:5.3.1-24.el5installed。
net-snmp-utils.i3861:5.3.1-24.el5installed。
现在可以容易地使用YUM下载并安装所有这些rpm软件包。
六、关于升级Gilbc的建议
Glibc库是Linux底层的运行库,其性能对于整个系统的运行有重要的意义。Glibc库包含了大量函数,其中的函数可大致分成两类,一类是与操作系统核心沟通的系统调用接口,它们作为功能型函数被调用,提供对Linux操作系统调用的包装与预处理。另外一类为一般的函数对象,它们提供了经常使用的功能的实现,作为工具型函数使用。在实践中,有不少软件就是依赖与Glibc版本才能安装并运行,说白了对于Glibc版本要求是版本高了不行,低了还不成。这些编译环境中的应用程序也和其它程序一样必须有运行的环境,我常遇到管理员在生产中给服务器装了最新的Linux发行版,结果应用软件装不上去,原因是Glibc的版本不对,有的是写在原发行版glibc上升级有的是降级,结果倒是整个系统的崩溃,实践经验告诉我,你只有选择相应Linux发行版里对应的glibc,例如我们单位的一个应用软件时在rhel3.0下开发的,那么就得要对应的发行版,换了别的就难说了,任何自己升级或降级Glibc来适应应用软件的做法都是不可取的,问题最后的解决方法是找到了RHEL3装上就解决了。在表一中,我把几个linux发行版原配的Glibc版本列出,供大家参考。
点击图片查看大图
Glibc库与核心功能组件
上图一说明:
GCC依赖于glibc
binutils依赖于glibc(binutils提供了一系列用来创建、管理和维护二进制目标文件的工具程序,如汇编(as)、连接(ld)、静态库归档(ar)、反汇编)
make依赖于glibc
头文件是在编译时候gcc所需要的,但本身都是一些文本文件,因此没有需要的运行环境。
常用工具依赖于glibc和各种需要用到的动态库。
下表一列出了多个重要Linux发行版的Glibc的情况。
Linux发行版Glibc版本。
Redhat9glibc-2.3.2-5。
Fedora1glibc-2.3.2。
RedhatEnterpriseLinuxAs3glibc-2.3.2-95。
RedhatEnterpriseLinuxAs4glibc-2.3.4。
RedhatEnterpriselinux5glibc-2.5-24。
RedhatEnterpriselinux6glibc-2.9。
Centos5.xglibc-2.5。
SuseLinuxEnterpriseServer9glibc-2.3.2-92。
SuseLinuxenterpriseServer10glibc-2.4.31.54。
SuseLinuxEnterpriseServer11glibc-2.9。
点击图片查看大图
Linux发行版glibc(32)位。
下面介绍几个查询glibc版本号的方法:
#ls–al/lib/libc*。
或者是用下面的命令也可以实现
#rpm–qp|grepglibc。
基于debian的系统通过dpkg–l|greplibc6也可以查到,总之一般都在/usr/share/doc目录下都能看到glibc的相关信息。
七、小结
大部分情况下,在遇到软件包依赖关系问题的时候,操作系统提供的文件名字与软件包名字都会有直接的联系。有可能文件的名字就是软件包的名字。但是有些时候文件的名字与软件包的名字会相差甚远。此时大部分系统管理员可能光凭文件名字无法找到对应的软件包。此时可以先在系统安装光盘里找,如果找到那时最佳选项,然后就需要借助笔者上面谈到的一些专业网站,去查询软件包的名字了。当系统管理员安装了某个软件之后,如果存在软件包之间的依赖关系,则最好能够拿本子或者通过其他手段记录下来。以便下次方便实用,注意工作中的积累,相信绝大部分的软件包依赖关系问题都会迎刃而解。