Prints "add-symbol-file..." command to load sections of the given module.
end
define lsallmodsects
set $mdl = modules.next
while $mdl != &modules
set $m = (struct module *)((char*)$mdl-(char*)(&((struct module*)0)->list))
lsmodsects $m
set $mdl = $mdl->next
end
end
document lsallmodsects
Calls lsmodsects on all modules.
end
汇编基础--X86篇
注意:某些内容不具备普遍性。比如给出的反汇编代码,在不同的优化等级下是不同的。但是在熟悉了典型的函数调用链反汇编代码,对于有变化的其他形式也就不难理解了。
用户手册
Intel® 64 and IA-32 Architectures Software Developer’s Manuals
http://www.intel.com/products/processor/manuals/index.htm
AT&T汇编格式
参考
“AT&T汇编语言与GCC内嵌汇编简介” http://blog.chinaunix.net/u2/73528/showart_1110874.html
[杂类文章]
“Linux Assembly and Disassembly an Introduction” http://www.milw0rm.com/papers/47
内联汇编
GCC-Inline-Assembly-HOWTO http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html
汇编与C函数的相互调用
调用链形成和参数传递
参考文章 [多如牛毛]
“Guide: Function Calling Conventions” http://www.delorie.com/djgpp/doc/ug/asm/calling.html
“Intel x86 Function-call Conventions - Assembly View” http://www.unixwiz.net/techtips/win32-callconv-asm.html
“C Function Call Conventions and the Stack” http://www.cs.umbc.edu/~chang/cs313.s02/stack.shtml
“The C Calling Convention and the 8086: Using the Stack Frame” http://www.et.byu.edu/groups/ece425web/stable/labs/StackFrame.html
“C Function Calling Convention” http://adamw-dev.blogspot.com/2007/05/c-function-calling-convention.html
“C函数调用在GNU汇编中的实现” http://www.unixresources.net/linux/clf/cpu/archive/00/00/59/75/597564.html
“函数调用的几个概念:_stdcall,_cdecl....” http://blog.chinaunix.net/u2/67530/showart_601750.html
“Calling conventions(调用规则)” http://www.bobd.cn/itschool/Program/delphi/200612/itschool_12084.html
[扩展,简要说明原理。并用实例解析]
x86终极参考
CHAPTER 6 PROCEDURE CALLS, INTERRUPTS, AND EXCEPTIONS of
IA-32 Intel_ Architecture Software Developer’s Manual Volume 1_ Basic Architecture.pdf http://download.intel.com/design/processor/manuals/253665.pdf
寄存器的角色与保护
寄存器的角色
1. %esp: 栈指针
指向栈的顶端,也就是指向栈的最后一个正在使用的元素。%esp的值隐式地受到几个机器指令的影响,比如push,pop,call,ret等。
2. %ebp: 基址指针
指向当前栈的基地址,有时也称为“帧指针”。与%esp不同的是,它必须显式地进行操作才能改变值。
3. %eip: 指令指针
保存着下一个被执行机器指令的地址。当CPU执行call指令时,%eip的值自动被保存到栈中。还有,任何一个“jump”跳转指令都会直接地改变%eip
两条规则
1. gcc要求在函数调用的前后,寄存器%ebx,%esi,%edi,%ebp,%esp,%ds, %es,%ss的值保持不变。所以被调用函数如果需要修改这些寄存器的值,被调用函数必须负责对它们进行保护。[后三个??]
2. gcc规定在函数调用的前后,寄存器%eax,%edx,%ecx的值可以改变。所以调用函数如果需要防止子函数破坏这三个寄存器的值,调用者必须在函数调用前自己负责保护它们。
我们注意到,是保护,不一定是保存。如果确认没用到某寄存器,那么该寄存器就不需要一定要有一个先保存到栈而后再恢复原值的过程。
这两条规则实际是定义了对系统资源使用的权限和义务。
第一条规则,是银行和借贷者的关系。有人向银行借了几千万,结果赌博全输光了。还钱的期限到了,银行的行长对借贷者说“没事,你回家吧。几千万而已,我拿我工资给你垫上”。我想这样的事决不会发生,行长一个电话110过去,借贷者一天后就把钱还清了。所以,这里,调用函数是银行行长,子函数是借贷者。
第二条规则,则是老爸和儿子的关系了。儿子对老爸说“老爸,解我100去买球鞋,我明天还你”。结果,第二天,老爸没钱吃饭了,问儿子“还钱”。儿子说“昨晚逛街碰到一个美女,请了一顿,把钱化光了”。老爸没法子,总不能把儿子绳以正法吧。怪只能怪自己事前没防这招咯。所以,这里,调用函数是老爸,子函数是儿子你。
返回值
1. Integers (of any size up to 32 bits) and pointers are returned in the %eax register.
2. Floating point values are returned in the 387 top-of-stack register, st(0).
3. Return values of type long long int are returned in %edx:%eax (the most significant word in %edx and the least significant in %eax).
4. Returning a structure is complicated and rarely useful; try to avoid it. (Note that this is different from returning a pointer to a structure.)
5. If your function returns void (e.g. no value), the contents of these registers are not used.调用链的形成
应用层实例解析
我们回头看看“寄存器的角色”这一小节,很快就能明白调用链的形成的本质。
调用链包含两方面的内容
1.返回地址的保存与恢复
2.旧栈帧的保存与恢复
因为在普通的调用形式中(call调用),返回地址的保存与恢复是由处理器机制本身保证的,不需人工维护。调用指令call的执行自动将call指令之下的指令地址压入栈中,被调用函数返回时,ret指令的执行会重新将返回地址从栈弹出传送到pc中。要求下面分析旧栈帧的保存与恢复。
旧栈帧的保存与恢复,无非就是要解决两大问题:
1. 建立新栈帧 这一步很简单,栈帧无非有两个头,底端和顶端。%esp指向栈的顶端,而%esp是不需要手工维护的,随着push,pop等指令,它自己就在改变自己。那么又怎么建立栈帧的底端呢?我们知道,栈底(也就是基址)是由%ebp指定的,在一个栈帧的整个生命周期里,%ebp的值都不变,也就是说,赋个合适的值给它就完事。怎么赋值就是问题所在了。我们知道,%esp指向栈中最后一个被使用的元素。所以,当我们正在使用(我们认为的)第一个元素时,把%esp的值赋给%ebp,%ebp不就是指向栈的基址了吗?
2. 保护旧栈帧的信息 同样的问题,保护旧栈帧的信息,就是保存旧栈帧指向底端和顶端的指针值,也就是旧%ebp,%esbp的值。当函数调用指令刚执行完,马上就要保护作案现场了。首先,push %ebp,这句就把旧栈帧的基地址保存在栈的顶端。此时,%esp指向的内存地址中,就放着旧栈帧的基地址的值。但是还不够啊,%esp是个不可靠的东西,它经常在变化,必须把这个地址放到一个不会隐式变化的寄存器中。于是选择了%ebp。mov %esp %ebp.这样,%ebp指向的内存地址中,就放着旧栈帧的基地址的值。这就解放了%esp,可以用%esp来动态指向新栈帧的顶端了。按照定义,%ebp所指向的地址是新栈帧的底端,也就是新栈帧的第一个元素,也就是说新栈帧第一个元素的值是旧栈帧基址。
但是注意,%ebp指向的地址再加4bytes的地址上,存放的是被调用函数的返回地址。在执行call指令时,call指令后面的那个指令的地址(也就是被调用函数的返回地址)被自动隐式地放到了栈中。
当子函数返回时,再按照上面文字进行逆操作,就能恢复旧栈帧的信息。
#include
void func()
{}
void funb()
{
func();
}
void funa()
{
funb();
}
int main()
{
funa();
}
-------
08048344 :
#include
void func()
{}
8048344: 55 push %ebp
8048345: 89 e5 mov %esp,%ebp
8048347: 5d pop %ebp
8048348: c3 ret
08048349 :
void funb()
{
8048349: 55 push %ebp
804834a: 89 e5 mov %esp,%ebp
func();
804834c: e8 f3 ff ff ff call 8048344
}
8048351: 5d pop %ebp
8048352: c3 ret
08048353 :
void funa()
{
8048353: 55 push %ebp
8048354: 89 e5 mov %esp,%ebp
funb();
8048356: e8 ee ff ff ff call 8048349
}
804835b: 5d pop %ebp
804835c: c3 ret
0804835d :
int main()
{
804835d: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048361: 83 e4 f0 and $0xfffffff0,%esp
8048364: ff 71 fc pushl -0x4(%ecx)
8048367: 55 push %ebp
8048368: 89 e5 mov %esp,%ebp
804836a: 51 push %ecx
funa();
804836b: e8 e3 ff ff ff call 8048353
}
8048370: 59 pop %ecx
8048371: 5d pop %ebp
8048372: 8d 61 fc lea -0x4(%ecx),%esp
8048375: c3 ret
8048376: 90 nop
8048377: 90 nop
8048378: 90 nop
8048379: 90 nop
804837a: 90 nop
804837b: 90 nop
804837c: 90 nop
804837d: 90 nop
804837e: 90 nop
804837f: 90 nop
func被调用后内存如下
| |
| | | | hight
| | | |
| +--------------/ |
+---+ main's %ebp |/ |
+-> +--------------+ --funa's frame |
| | ret to funa | / |
| +--------------+X |
+---+ funa's %ebp | / |
+-->+--------------+ ---funb's frame |
| | ret to funb | / |
| +--------------+ |
+---+ funb's %ebp |<---func's frame | low
%esp--> +--------------+<---- %ebp v
| |
| |
| |
| |
| |
内核层实例解析
栈帧结构与参数传递
栈元素引用的就近原则
为了说明就近原则,我们先看看典型和全面的栈帧是怎样的。函数caller调用子函数callee所形成的栈帧。
1. 从被调用的子函数callee来看,获取caller的传递的实参,以及建立自身本地变量时,因为内存地址都靠近栈帧的基址,所以这两种引用都是利用%ebp加上偏移量的形式。
2. 相反,主函数在调用子函数前,在为子函数准备实参时,因为实参位于栈帧末端,所以对实参的引用都是利用%esp加上偏移量的形式(没画出来)
caller's frame pointer
|
| | |
| | |
| | |
| +-------------------+
| | caller saved |
| | registers |
| | %eax,%ecx,%edx |
| | (as needed) |
| +-------------------+
| | argument #3 | [%ebp+16]
| +-------------------+
| | argument #2 | [%ebp+12]
| +-------------------+
| | argument #1 | [%ebp+8]
| +-------------------+
| | return address |
| +-------------------+ -----
+-----+ caller's %ebp |<---%ebp /
+-------------------+ /
| local var #1 | [%ebp-4] /
+-------------------+ |
| local var #2 | [%ebp-8] |
+-------------------+ |
| temporary | |
| storage | |
+-------------------+
| callee saved | callee stack frame
| registers | |
| %ebx,%esi,%edi | |
| (as needed) | |
+-------------------+ |
| | |
| | |
| | /
| |<----%esp /
|
caller:调用者 callee:被调用者
完整的调用过程
函数caller调用子函数callee,这是应用层的普通函数调用过程。如果是远调用,跨态调用要考虑的东西更多。但这个例子已经充分展示了调用过程的繁复部分。
函数调用前调用者的动作
1.%eax,%edx,%ecx入栈(可选)
2.子函数的参数入栈
函数调用 call callee
call机器指令,原子性自动地完成了两种任务.
1.%eip入栈, 保存了callee函数的返回地址
2.callee的函数地址传递到%eip.
所以下一指令就从callee函数的第一指令开始运行。控制权转移给callee
函数调用后被调用者的动作
1.保存caller栈帧基址 push %ebp
2.建立callee栈帧基址 mov %esp,%ebp
3.分配本地变量和临时存储的空间 sub $XXX, %esp
4.本地变量赋值
5.%ebx,%esi,%edi入栈(可选)
调用返回前被调用者的动作
1.%ebx,%esi,%edi还原(出栈,可选)
2.释放本地变量和临时存储的栈空间mov %ebp,%esp
3.还原caller栈帧的基址 pop %ebp
或者2.3.步用一条元语指令完成 leave
4.调用返回 ret
该指令把存放于栈的返回地址取出(出栈),存放到%eip中。下一指令就从call callee指令的下一指令开始运行。控制权返回给caller
调用返回后调用者的动作
1.释放存放callee参数的栈空间 add $XXX, %esp
2.转移%eax的值(子函数的返回值,可选)
3.还原%eax,%edx,%ecx(出栈,可选)
应用层实例解析
应用层参数的传入: 用户层参数的传递是利用栈来完成的。函数右边的参数先入栈,位于栈的高地址。反之, 函数左边的参数后入栈,位于栈的低地址。
例子请看 “C难点的汇编解释”
内核层实例解析
内核层参数的传入: 混合使用寄存器和栈来传递参数。当参数个数不多于3个时,参数从左到右依次传递到%eax, %edx, %ecx.当参数个数多于3时,从第4个起的其余参数通过栈传递。同样,函数右边的参数先入栈,位于栈的高地址。反之, 函数左边的参数后入栈,位于栈的低地址。
系统调用实例解析
系统调用的参数传递:[以后再看]
C库函数
ssize_t read(int fd, void *buf, size_t count);
000b6a30 <__read>:
b6a30: 65 83 3d 0c 00 00 00 cmpl $0x0,%gs:0xc
b6a37: 00
b6a38: 75 1d jne b6a57 <__read+0x27>
b6a3a: 53 push %ebx
b6a3b: 8b 54 24 10 mov 0x10(%esp),%edx //count
b6a3f: 8b 4c 24 0c mov 0xc(%esp),%ecx //buf
b6a43: 8b 5c 24 08 mov 0x8(%esp),%ebx //fd
b6a47: b8 03 00 00 00 mov $0x3,%eax //系统调用号
b6a4c: cd 80 int $0x80
b6a4e: 5b pop %ebx
b6a4f: 3d 01 f0 ff ff cmp $0xfffff001,%eax
b6a54: 73 2d jae b6a83 <__read+0x53>
b6a56: c3 ret
b6a57: e8 14 ae 01 00 call d1870
b6a5c: 50 push %eax
b6a5d: 53 push %ebx
b6a5e: 8b 54 24 14 mov 0x14(%esp),%edx
b6a62: 8b 4c 24 10 mov 0x10(%esp),%ecx
b6a66: 8b 5c 24 0c mov 0xc(%esp),%ebx
b6a6a: b8 03 00 00 00 mov $0x3,%eax
b6a6f: cd 80 int $0x80
b6a71: 5b pop %ebx
b6a72: 87 04 24 xchg %eax,(%esp)
b6a75: e8 c6 ad 01 00 call d1840
b6a7a: 58 pop %eax
b6a7b: 3d 01 f0 ff ff cmp $0xfffff001,%eax
b6a80: 73 01 jae b6a83 <__read+0x53>
b6a82: c3 ret
b6a83: e8 8e 5a 04 00 call fc516 <__frame_state_for+0xb96>
b6a88: 81 c1 6c e5 07 00 add $0x7e56c,%ecx
b6a8e: 8b 89 e0 ff ff ff mov -0x20(%ecx),%ecx
b6a94: 31 d2 xor %edx,%edx
b6a96: 29 c2 sub %eax,%edx
b6a98: 65 03 0d 00 00 00 00 add %gs:0x0,%ecx
b6a9f: 89 11 mov %edx,(%ecx)
b6aa1: 83 c8 ff or $0xffffffff,%eax
b6aa4: eb dc jmp b6a82 <__read+0x52>
b6aa6: 90 nop
调用号#define __NR_read 3
(gdb) disass sys_read
Dump of assembler code for function sys_read:
0xc017585a : push %ebp
0xc017585b : mov %esp,%ebp
0xc017585d : push %esi
0xc017585e : mov $0xfffffff7,%esi
0xc0175863 : push %ebx
0xc0175864 : sub $0xc,%esp
0xc0175867 : mov 0x8(%ebp),%eax
0xc017586a : lea -0xc(%ebp),%edx
0xc017586d : call 0xc0175f65
0xc0175872 : test %eax,%eax
0xc0175874 : mov %eax,%ebx
0xc0175876 : je 0xc01758b1
0xc0175878 : mov 0x24(%ebx),%edx
0xc017587b : mov 0x20(%eax),%eax
0xc017587e : mov 0x10(%ebp),%ecx
0xc0175881 : mov %edx,-0x10(%ebp)
0xc0175884 : mov 0xc(%ebp),%edx
0xc0175887 : mov %eax,-0x14(%ebp)
0xc017588a : lea -0x14(%ebp),%eax
0xc017588d : push %eax
0xc017588e : mov %ebx,%eax
0xc0175890 : call 0xc01753c1
0xc0175895 : mov -0x10(%ebp),%edx
0xc0175898 : mov %eax,%esi
0xc017589a : mov -0x14(%ebp),%eax
0xc017589d : mov %edx,0x24(%ebx)
0xc01758a0 : mov %eax,0x20(%ebx)
0xc01758a3 : cmpl $0x0,-0xc(%ebp)
0xc01758a7 : pop %eax
0xc01758a8 : je 0xc01758b1
0xc01758aa : mov %ebx,%eax
0xc01758ac : call 0xc0175eae
0xc01758b1 : lea -0x8(%ebp),%esp
0xc01758b4 : mov %esi,%eax
0xc01758b6 : pop %ebx
0xc01758b7 : pop %esi
0xc01758b8 : pop %ebp
0xc01758b9 : ret
End of assembler dump.
(gdb) list fget_light
313 * holds a refcnt to that file. That check has to be done at fget() only
314 * and a flag is returned to be passed to the corresponding fput_light().
315 * There must not be a cloning between an fget_light/fput_light pair.
316 */
317 struct file *fget_light(unsigned int fd, int *fput_needed)
来自2.6.11
378 #define _syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, /
379 type5,arg5,type6,arg6) /
380 type name (type1 arg1,type2 arg2,type3 arg3,type4 arg4,type5 arg5,type6 arg6) /
381 { /
382 long __res; /
383 __asm__ volatile ("push %%ebp ; movl %%eax,%%ebp ; movl %1,%%eax ; int $0x80 ; pop %%ebp" /
384 : "=a" (__res) /
385 : "i" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), /
386 "d" ((long)(arg3)),"S" ((long)(arg4)),"D" ((long)(arg5)), /
387 "0" ((long)(arg6))); /
388 __syscall_return(type,__res); /
389 }
调用链回溯的代码实现
内核中(x86)对调用链的回溯的代码实现在文件dumpstack_32.c文件中。主要函数是dump_trace和print_context_stack.
待解释
C难点的汇编解释
例1
if ... else if
这个例子有人看来也许是非常非常地简单,但就这个例子,有的人还真给我考”倒”了。他的回话是“还真没见过这样子的代码”。但是,这样的代码在内核中比比皆是,比如后面附上的函数代码 do_path_lookup。如果对if ... else if 理解有偏差,对内核代码的逻辑理解根本就是差以千里。
#include
int main()
{
int i = 1;
int j = 2;
if (i == 1)
printf("i,ok/n");
else if (j == 2)
printf("j,ok/n");
return 0;
}
这个例子,有人会疑问为什么”j,ok”没打印出来。现在我们分析下它的汇编代码
08048374 :
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048378: 83 e4 f0 and $0xfffffff0,%esp
804837b: ff 71 fc pushl -0x4(%ecx)
804837e: 55 push %ebp
804837f: 89 e5 mov %esp,%ebp //以上汇编码保存旧栈帧信息,建立新栈帧
8048381: 51 push %ecx //%ecx入栈保护
8048382: 83 ec 14 sub $0x14,%esp //建立本地变量栈空间,以及子函数实参栈空间
8048385: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp) //变量i赋值,记得本地变量的地址靠近栈帧的基地址,所以用%ebp引用
804838c: c7 45 f4 02 00 00 00 movl $0x2,-0xc(%ebp) //变量j赋值
8048393: 83 7d f8 01 cmpl $0x1,-0x8(%ebp) //i和1比较
8048397: 75 0e jne 80483a7 //如果i-1不等0,跳到地址80483a7执行。否则继续执行下面指令
8048399: c7 04 24 90 84 04 08 movl $0x8048490,(%esp) //printf函数第一个参数入栈,它的栈空间之前已经建好。
//记得子函数的实参空间靠近栈顶,所以引用实参用%esp
80483a0: e8 2f ff ff ff call 80482d4 //调用printf
80483a5: eb 12 jmp 80483b9 //printf返回后,接着执行这个指令,将跳到地址80483b9继续运行
80483a7: 83 7d f4 02 cmpl $0x2,-0xc(%ebp)
80483ab: 75 0c jne 80483b9
80483ad: c7 04 24 95 84 04 08 movl $0x8048495,(%esp)
80483b4: e8 1b ff ff ff call 80482d4
80483b9: b8 00 00 00 00 mov $0x0,%eax //%eax赋值0,%eax放的也就是main函数返回结果
80483be: 83 c4 14 add $0x14,%esp //撤销新栈帧的本地变量栈空间,以及子函数实参栈空间
80483c1: 59 pop %ecx //恢复保存的旧%ecx的值
80483c2: 5d pop %ebp //以下汇编码都是恢复旧栈帧的信息,main函数返回等
80483c3: 8d 61 fc lea -0x4(%ecx),%esp
80483c6: c3 ret
经过上面的汇编代码分析,可见c代码块
else if (j == 2)
printf("j,ok/n");
对应的汇编代码是:
80483a7: 83 7d f4 02 cmpl $0x2,-0xc(%ebp)
80483ab: 75 0c jne 80483b9
80483ad: c7 04 24 95 84 04 08 movl $0x8048495,(%esp)
80483b4: e8 1b ff ff ff call 80482d4
上面的代码指令根本就没有机会运行。
结论,一个if ... else if ..else..
if (判断语句1)
代码块1
else if (判断语句2)
代码块2;
else if ....
..
else 代码块N;
语句块1,2..N的运行机会是一种互斥的关系。当然它们的“机会优先级”是不一样的。语句块1,2..N只有一个有被运行的机会,如果没有else甚至可能没有一个语句块能被运行。
内核代码实例
static int do_path_lookup(int dfd, const char *name,
unsigned int flags, struct nameidata *nd)
{
int retval = 0;
int fput_needed;
struct file *file;
struct fs_struct *fs = current->fs;
nd->last_type = LAST_ROOT; /* if there are only slashes... */
nd->flags = flags;
nd->depth = 0;
if (*name=='/') {
read_lock(&fs->lock);
if (fs->altroot.dentry && !(nd->flags & LOOKUP_NOALT)) {
nd->path = fs->altroot;
path_get(&fs->altroot);
read_unlock(&fs->lock);
if (__emul_lookup_dentry(name,nd))
goto out; /* found in altroot */
read_lock(&fs->lock);
}
nd->path = fs->root;
path_get(&fs->root);
read_unlock(&fs->lock);
} else if (dfd == AT_FDCWD) {
read_lock(&fs->lock);
nd->path = fs->pwd;
path_get(&fs->pwd);
read_unlock(&fs->lock);
} else {
struct dentry *dentry;
file = fget_light(dfd, &fput_needed);
retval = -EBADF;
if (!file)
goto out_fail;
dentry = file->f_path.dentry;
retval = -ENOTDIR;
if (!S_ISDIR(dentry->d_inode->i_mode))
goto fput_fail;
retval = file_permission(file, MAY_EXEC);
if (retval)
goto fput_fail;
nd->path = file->f_path;
path_get(&file->f_path);
fput_light(file, fput_needed);
}
retval = path_walk(name, nd);
out:
if (unlikely(!retval && !audit_dummy_context() && nd->path.dentry &&
nd->path.dentry->d_inode))
audit_inode(name, nd->path.dentry);
out_fail:
return retval;
fput_fail:
fput_light(file, fput_needed);
goto out_fail;
}
例2
短路逻辑算法。
这样的例子在内核代码中也是非常地多,一般用在短的函数或宏中。
#include
int main()
{
int a = 1;
int b = 2;
if (a || ++b)
printf("%d/n", b);
return 0;
}
这个例子,有人会疑问为什么b的值没有变化,还是为2。现在我们分析下它的汇编代码
08048374 :
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048378: 83 e4 f0 and $0xfffffff0,%esp
804837b: ff 71 fc pushl -0x4(%ecx)
804837e: 55 push %ebp
804837f: 89 e5 mov %esp,%ebp //以上汇编码保存旧栈帧信息,建立新栈帧
8048381: 51 push %ecx //%ecx入栈保护
8048382: 83 ec 24 sub $0x24,%esp //创建本地变量和子函数实参的栈空间(实际上没全部使用到)
8048385: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp) //变量a赋值,记得本地变量的地址靠近栈帧的基地址,所以用%ebp引用
804838c: c7 45 f4 02 00 00 00 movl $0x2,-0xc(%ebp) //变量b赋值
8048393: 83 7d f8 00 cmpl $0x0,-0x8(%ebp) //变量a和0比较,其实就是判断“表达式 a”是不是为假
8048397: 75 0a jne 80483a3 //a-0如果不等0,也就是a为真时就跳到地址80483a3执行。
//已经知道a==1,表达式a为真,所以将跳到地址80483a3执行
8048399: 83 45 f4 01 addl $0x1,-0xc(%ebp)
804839d: 83 7d f4 00 cmpl $0x0,-0xc(%ebp)
80483a1: 74 13 je 80483b6
80483a3: 8b 45 f4 mov -0xc(%ebp),%eax //把变量b的值放到临时寄存器%eax
80483a6: 89 44 24 04 mov %eax,0x4(%esp) //接着把它作为printf函数第二个实参入栈,
//记得子函数的实参空间靠近栈顶,所以引用实参用%esp
80483aa: c7 04 24 90 84 04 08 movl $0x8048490,(%esp) //printf函数第一个实参入栈。记得X86下用户层的子函数参数
//是保存到栈的,而且是从右到左依次入栈
80483b1: e8 22 ff ff ff call 80482d8 //调用printf函数
80483b6: b8 00 00 00 00 mov $0x0,%eax //%eax赋值0,%eax放的也就是main函数返回结果
80483bb: 83 c4 24 add $0x24,%esp //撤销新栈帧的本地变量栈空间,以及子函数实参栈空间
80483be: 59 pop %ecx //恢复保存的旧%ecx的值
80483bf: 5d pop %ebp //以下汇编码都是恢复旧栈帧的信息,main函数返回等
80483c0: 8d 61 fc lea -0x4(%ecx),%esp
80483c3: c3 ret
分析可见C语句 if (a || ++b)中的++b对应的汇编码是
8048399: 83 45 f4 01 addl $0x1,-0xc(%ebp)
804839d: 83 7d f4 00 cmpl $0x0,-0xc(%ebp)
80483a1: 74 13 je 80483b6
可是因为a==1,表达式a已经为真,++b这个语句,也就是上面的汇编码,根本就没运行。所以变量b的值没有自增,还是保持为2。
结论
表达式 a, b
a || b: 如果a为真,b就不管;如果运行到b,a必已是假
a && b: 如果a为假,b就不管;如果运行到b,a必已是真
内核代码实例
static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
......
i = major_to_index(major);
for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major ||
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) ||
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;
.....
}
例3
自增自减
自增自减,以及增减的前后问题。这类代码在内核数不胜数。理解稍有偏差,就会产生“边界问题”,或者在条件判断时理解出错。
#include
int main()
{
int i = -1;
if (!i++) {
printf("inner: %d/n", i);
}
printf("outer: %d/n", i);
return 0;
}
汇编代码
08048374 :
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048378: 83 e4 f0 and $0xfffffff0,%esp
804837b: ff 71 fc pushl -0x4(%ecx)
804837e: 55 push %ebp
804837f: 89 e5 mov %esp,%ebp
8048381: 51 push %ecx
8048382: 83 ec 24 sub $0x24,%esp
8048385: c7 45 f8 ff ff ff ff movl $0xffffffff,-0x8(%ebp)
804838c: 83 45 f8 01 addl $0x1,-0x8(%ebp)
8048390: 83 7d f8 01 cmpl $0x1,-0x8(%ebp)
8048394: 75 13 jne 80483a9
8048396: 8b 45 f8 mov -0x8(%ebp),%eax
8048399: 89 44 24 04 mov %eax,0x4(%esp)
804839d: c7 04 24 90 84 04 08 movl $0x8048490,(%esp)
80483a4: e8 2f ff ff ff call 80482d8
80483a9: 8b 45 f8 mov -0x8(%ebp),%eax
80483ac: 89 44 24 04 mov %eax,0x4(%esp)
80483b0: c7 04 24 9b 84 04 08 movl $0x804849b,(%esp)
80483b7: e8 1c ff ff ff call 80482d8
80483bc: b8 00 00 00 00 mov $0x0,%eax
80483c1: 83 c4 24 add $0x24,%esp
80483c4: 59 pop %ecx
80483c5: 5d pop %ebp
80483c6: 8d 61 fc lea -0x4(%ecx),%esp
80483c9: c3 ret
80483ca: 90 nop
内核代码实例
int platform_add_devices(struct platform_device **devs, int num)
{
int i, ret = 0;
for (i = 0; i < num; i++) {
ret = platform_device_register(devs[i]);
if (ret) {
while (--i >= 0) /*没错,devs[i]没注册成功的话,从devs[i-1]起反注册*/
platform_device_unregister(devs[i]);
break;
}
}
return ret;
}
例14
函数指针
解释在“穿越交叉索引工具的盲区”→函数指针
#include
int main()
{
int myfunc(int a, int b)
{
int c = a + b;
printf("%d/n", c);
return 0;
}
int (*funa)(int, int) = myfunc;
int (*funb)(int, int) = &myfunc;
int (*func)(int, int) = (int (*)(int, int))myfunc;
int (*fund)(int, int) = (int (*)(int, int))(&myfunc);
myfunc(1, 2);
funa(3, 4);
funb(5, 6);
func(7, 8);
fund(9, 10);
return 0;
}
编译:
$ gcc -g -Wall fuk.c //注意,没任何警告
int main()
{
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
....省略
int (*funa)(int, int) = myfunc;
8048385: c7 45 f8 13 84 04 08 movl $0x8048413,-0x8(%ebp)
int (*funb)(int, int) = &myfunc;
804838c: c7 45 f4 13 84 04 08 movl $0x8048413,-0xc(%ebp)
int (*func)(int, int) = (int (*)(int, int))myfunc;
8048393: c7 45 f0 13 84 04 08 movl $0x8048413,-0x10(%ebp)
int (*fund)(int, int) = (int (*)(int, int))(&myfunc);
804839a: c7 45 ec 13 84 04 08 movl $0x8048413,-0x14(%ebp)
myfunc(1, 2);
...省略
funa(3, 4);
80483b5: c7 44 24 04 04 00 00 movl $0x4,0x4(%esp)
80483bc: 00
80483bd: c7 04 24 03 00 00 00 movl $0x3,(%esp)
80483c4: 8b 45 f8 mov -0x8(%ebp),%eax
80483c7: ff d0 call *%eax
funb(5, 6);
....省略,funb, func,fund汇编码和funa完全相同
return 0;
8048405: b8 00 00 00 00 mov $0x0,%eax
}
804840a: 83 c4 24 add $0x24,%esp
...省略
08048413 :
#include
int main()
{
int myfunc(int a, int b)
{
8048413: 55 push %ebp
.....省略
}
xxx@ubuntu:~/dt/test$ gdb a.out
GNU gdb 6.8-debian
...
(gdb) list
1 #include
......
17 funa(3, 4);
....
20
(gdb) b 17
(gdb) r
Starting program: /home/xxx/桌面/test/a.out
Breakpoint 1, main () at fuck.c:17
17 funa(3, 4);
(gdb) display/i $pc
1: x/i $pc
0x80483b5 : movl $0x4,0x4(%esp)
(gdb) stepi
0x080483bd 17 funa(3, 4);
1: x/i $pc
0x80483bd : movl $0x3,(%esp)
(gdb)
0x080483c4 17 funa(3, 4);
1: x/i $pc
0x80483c4 : mov -0x8(%ebp),%eax
(gdb)
0x080483c7 17 funa(3, 4);
1: x/i $pc
0x80483c7 : call *%eax
(gdb) p/x $eax
$4 = 0x8048413
(gdb) info line *0x8048413
Line 6 of "fuck.c" starts at address 0x8048413 and ends at 0x8048419 .
(gdb)
优化级别的影响
这部分内容有点偏题,没必要这么钻牛角尖。但是为了说明“调试用的代码和实际运行的代码是不一样”的这个事实以及因为代码优化导致的“非理想状态”的调用链问题(见“内核初窥”),有必要用观察一个实例,以便有个直观的印象。
首先应该知道,有没有指定调试选项-g(–debug),在相同优先级下生成的代码都是一样的。差别只是,指定-g后,多生成了一个调试表。
优化选项
下面文字来自“ARM 系列应用技术完全手册”
使用-Onum选择编译器的优化级别。优化级别分别有
-O0: 除一些简单的代码编号外,关闭所有优化,该选项可提供最直接的优化信息。
-O1: 关闭严重影响调试效果的优化功能。使用该编译选项,编译器会移除程序中未使用到的内联函数和静态函数。如果于–debug(也就是-g)一起使用,该选项可以在较好的代码密度下,给出最佳调试视图。
-O2: 生成充分优化代码。如果与–debug一起使用,调试效果可能不令人满意,因为对目标代码到源代码的映射可能因为代码优化而发生变化。如果不生成调试表,这是默认优化级别。
-O3: 最高优化级别。使用该优化级别,使生成的代码在时间和空间上寻求平衡。
例子
#include
int add(int a, int b)
{
return (a + b);
}
void funa()
{
int a = 3 + 4;
int b;
printf("%d/n", a);
b = add(5,6);
printf("%d/n", b);
}
int main()
{
int m = 1 + 2;
printf("%d/n", m);
funa();
}
$ gcc -g -O0 src.c (或者不指定优化选项: gcc -g src.c,编译出的机器码一样)
$ objdump -d a.out
得到一个结论:如果指定了-g而没指定优化等级,那么默认优化等级是最低的-O0
08048374 :
8048374: 55 push %ebp
8048375: 89 e5 mov %esp,%ebp
8048377: 8b 45 0c mov 0xc(%ebp),%eax
804837a: 03 45 08 add 0x8(%ebp),%eax
804837d: 5d pop %ebp
804837e: c3 ret
0804837f :
804837f: 55 push %ebp
8048380: 89 e5 mov %esp,%ebp //保存旧栈帧,建立新栈帧
8048382: 83 ec 18 sub $0x18,%esp //分配栈帧空间,注意分配了$0x18
8048385: c7 45 fc 07 00 00 00 movl $0x7,-0x4(%ebp) //-0x4(%ebp)是本地变量a的地址,int a = 3 + 4;
//注意编译器已经完成了计算
804838c: 8b 45 fc mov -0x4(%ebp),%eax //a放到临时寄存器%eax
804838f: 89 44 24 04 mov %eax,0x4(%esp) //接着作为printf第二个参数入栈
8048393: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp) //printf第一个参数入栈
804839a: e8 39 ff ff ff call 80482d8 //printf("%d/n", a);
804839f: c7 44 24 04 06 00 00 movl $0x6,0x4(%esp) //add(5,6);第二个参数入栈
80483a6: 00
80483a7: c7 04 24 05 00 00 00 movl $0x5,(%esp) //add(5,6);第一个参数入栈
80483ae: e8 c1 ff ff ff call 8048374 //调用add
80483b3: 89 45 f8 mov %eax,-0x8(%ebp) //-0x8(%ebp)是本地变量b的地址,b = add(5,6);
80483b6: 8b 45 f8 mov -0x8(%ebp),%eax //b放到临时寄存器%eax
80483b9: 89 44 24 04 mov %eax,0x4(%esp) //接着作为printf第二个参数入栈
80483bd: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp) //printf第一个参数入栈
80483c4: e8 0f ff ff ff call 80482d8 //printf("%d/n", b);
80483c9: c9 leave //撤销新栈帧空间
80483ca: c3 ret //funa返回
080483cb :
80483cb: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483cf: 83 e4 f0 and $0xfffffff0,%esp
80483d2: ff 71 fc pushl -0x4(%ecx)
80483d5: 55 push %ebp
80483d6: 89 e5 mov %esp,%ebp
80483d8: 51 push %ecx
80483d9: 83 ec 24 sub $0x24,%esp
80483dc: c7 45 f8 03 00 00 00 movl $0x3,-0x8(%ebp)
80483e3: 8b 45 f8 mov -0x8(%ebp),%eax
80483e6: 89 44 24 04 mov %eax,0x4(%esp)
80483ea: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp)
80483f1: e8 e2 fe ff ff call 80482d8
80483f6: e8 84 ff ff ff call 804837f
80483fb: 83 c4 24 add $0x24,%esp
80483fe: 59 pop %ecx
80483ff: 5d pop %ebp
8048400: 8d 61 fc lea -0x4(%ecx),%esp
8048403: c3 ret
$ gcc -g -O1 src.c
$ objdump -d a.out
08048374 :
8048374: 55 push %ebp
8048375: 89 e5 mov %esp,%ebp
8048377: 8b 45 0c mov 0xc(%ebp),%eax
804837a: 03 45 08 add 0x8(%ebp),%eax
804837d: 5d pop %ebp
804837e: c3 ret
0804837f : //funa与-O0相比,没有了向本地变量a,b赋值的过程。
//代码量少了,分配的栈帧空间也小了。
804837f: 55 push %ebp
8048380: 89 e5 mov %esp,%ebp
8048382: 83 ec 08 sub $0x8,%esp //分配栈帧空间,注意分配了$0x8,比-O0下小了
8048385: c7 44 24 04 07 00 00 movl $0x7,0x4(%esp) //printf("%d/n", a);的第二个参数入栈。
//注意,与-O0相比,没有向本地变量a赋值的过程。
804838c: 00
804838d: c7 04 24 c0 84 04 08 movl $0x80484c0,(%esp)
8048394: e8 3f ff ff ff call 80482d8 //printf("%d/n", a);
8048399: c7 44 24 04 06 00 00 movl $0x6,0x4(%esp)
80483a0: 00
80483a1: c7 04 24 05 00 00 00 movl $0x5,(%esp)
80483a8: e8 c7 ff ff ff call 8048374 //add(5,6);
80483ad: 89 44 24 04 mov %eax,0x4(%esp) //add的返回结果作为printf("%d/n", b);的第二个参数入栈。
//注意,与-O0相比,没有向本地变量b赋值的过程。
80483b1: c7 04 24 c0 84 04 08 movl $0x80484c0,(%esp)
80483b8: e8 1b ff ff ff call 80482d8 //printf("%d/n", b);
80483bd: c9 leave
80483be: c3 ret
080483bf :
80483bf: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483c3: 83 e4 f0 and $0xfffffff0,%esp
80483c6: ff 71 fc pushl -0x4(%ecx)
80483c9: 55 push %ebp
80483ca: 89 e5 mov %esp,%ebp
80483cc: 51 push %ecx
80483cd: 83 ec 14 sub $0x14,%esp
80483d0: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)
80483d7: 00
80483d8: c7 04 24 c0 84 04 08 movl $0x80484c0,(%esp)
80483df: e8 f4 fe ff ff call 80482d8
80483e4: e8 96 ff ff ff call 804837f
80483e9: 83 c4 14 add $0x14,%esp
80483ec: 59 pop %ecx
80483ed: 5d pop %ebp
80483ee: 8d 61 fc lea -0x4(%ecx),%esp
80483f1: c3 ret
$ gcc -g -O2 src.c
$ objdump -d a.out
我们应该知道,如果没有指定-g和优化选项,那么默认的优化等级就是-O2
08048380 :
8048380: 55 push %ebp
8048381: 89 e5 mov %esp,%ebp
8048383: 8b 45 0c mov 0xc(%ebp),%eax
8048386: 03 45 08 add 0x8(%ebp),%eax
8048389: 5d pop %ebp
804838a: c3 ret
804838b: 90 nop
804838c: 8d 74 26 00 lea 0x0(%esi),%esi
08048390 :
8048390: 55 push %ebp
8048391: 89 e5 mov %esp,%ebp
8048393: 83 ec 08 sub $0x8,%esp
8048396: c7 44 24 04 07 00 00 movl $0x7,0x4(%esp)
804839d: 00
804839e: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp)
80483a5: e8 2e ff ff ff call 80482d8
80483aa: c7 44 24 04 06 00 00 movl $0x6,0x4(%esp)
80483b1: 00
80483b2: c7 04 24 05 00 00 00 movl $0x5,(%esp)
80483b9: e8 c2 ff ff ff call 8048380
80483be: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp) //第二个参数入栈
80483c5: 89 44 24 04 mov %eax,0x4(%esp) //第一个参数入栈。注意和-O1相比,参数在栈帧空间的位置没变,
//但是入栈指令的执行顺序有变。
80483c9: e8 0a ff ff ff call 80482d8 //printf("%d/n", b);
80483ce: c9 leave
80483cf: c3 ret
080483d0 :
80483d0: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483d4: 83 e4 f0 and $0xfffffff0,%esp
80483d7: ff 71 fc pushl -0x4(%ecx)
80483da: 55 push %ebp
80483db: 89 e5 mov %esp,%ebp
80483dd: 51 push %ecx
80483de: 83 ec 14 sub $0x14,%esp
80483e1: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)
80483e8: 00
80483e9: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp)
80483f0: e8 e3 fe ff ff call 80482d8
80483f5: e8 96 ff ff ff call 8048390
80483fa: 83 c4 14 add $0x14,%esp
80483fd: 59 pop %ecx
80483fe: 5d pop %ebp
80483ff: 8d 61 fc lea -0x4(%ecx),%esp
8048402: c3 ret
$ gcc -g -O3 src.c
$ objdump -d a.out
048380 :
8048380: 55 push %ebp
8048381: 89 e5 mov %esp,%ebp
8048383: 8b 45 0c mov 0xc(%ebp),%eax
8048386: 03 45 08 add 0x8(%ebp),%eax
8048389: 5d pop %ebp
804838a: c3 ret
804838b: 90 nop
804838c: 8d 74 26 00 lea 0x0(%esi),%esi
08048390 : //与-O2相比,对函数add()的调用被编译器优化消失
8048390: 55 push %ebp
8048391: 89 e5 mov %esp,%ebp
8048393: 83 ec 08 sub $0x8,%esp
8048396: c7 44 24 04 07 00 00 movl $0x7,0x4(%esp)
804839d: 00
804839e: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483a5: e8 2e ff ff ff call 80482d8
80483aa: c7 44 24 04 0b 00 00 movl $0xb,0x4(%esp) //注意,与-O2相比,b = add(5,6);被优化掉了。
//之前应该有个优化为内联函数的过程,但因为add函数
//太简单,被直接计算了结果。(猜想)
//编译器直接计算出它的结果$0xb,也就是11
80483b1: 00
80483b2: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483b9: e8 1a ff ff ff call 80482d8 //printf("%d/n", b);
80483be: c9 leave
80483bf: c3 ret
080483c0 :
80483c0: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483c4: 83 e4 f0 and $0xfffffff0,%esp
80483c7: ff 71 fc pushl -0x4(%ecx)
80483ca: 55 push %ebp
80483cb: 89 e5 mov %esp,%ebp
80483cd: 51 push %ecx
80483ce: 83 ec 14 sub $0x14,%esp
80483d1: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)
80483d8: 00
80483d9: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483e0: e8 f3 fe ff ff call 80482d8
80483e5: c7 44 24 04 07 00 00 movl $0x7,0x4(%esp)
80483ec: 00
80483ed: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483f4: e8 df fe ff ff call 80482d8
80483f9: c7 44 24 04 0b 00 00 movl $0xb,0x4(%esp)
8048400: 00
8048401: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp)
8048408: e8 cb fe ff ff call 80482d8
804840d: 83 c4 14 add $0x14,%esp
8048410: 59 pop %ecx
8048411: 5d pop %ebp
8048412: 8d 61 fc lea -0x4(%ecx),%esp
8048415: c3 ret
汇编基础--ARM篇
说明:
1. 部分内容和X86的重复,重复部分请参考X86的内容。
2. 某些内容不具备普遍性。比如给出的反汇编代码,在不同的优化等级下是不同的。但是在熟悉了典型的函数调用链反汇编代码,对于有变化的其他形式也就不难理解了。
用户手册
ARM7TDMI Technical Reference Manual
ARM920T Technical Reference Manual
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.home/index.html
指令速查 http://www.arm.com/pdfs/QRC0001H_rvct_v2.1_arm.pdf
调用链形成和参数传递
注意:arm体系过程调用的文字说明部分,都是依据AAPCS标准。
壮观的标准
参考:
AAPCS
Procedure Call Standard for the ARM Architecture
http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042b/IHI0042B_aapcs.pdf
终于在“ARM Procedure Call Standard”中找到了答案
PCS Procedure Call Standard.
AAPCS Procedure Call Standard for the ARM Architecture (this standard).
APCS ARM Procedure Call Standard (obsolete).
TPCS Thumb Procedure Call Standard (obsolete).
ATPCS ARM-Thumb Procedure Call Standard (precursor to this standard).
PIC, PID Position-independent code, position-independent data.
下面的标准已过时
APCS
ARM Procedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0041c/BGBGFIDA.html
Using the ARM Procedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0040d/Chdbceig.html
APCS 简介http://www.bsdmap.com/UNIX_html/ARM/apcsintro.html#01
TPCS
Thumb Procedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0041c/BCEEAHAF.html
Using the Thumb Procedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0040d/Cihdbchi.html
ATPCS
About the ARM-Thumb Procedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0056d/Bcffcieh.html
别名的烦恼
arm体系的函数调用标准换了好几个版本,对寄存器的别名也是不一样。不同的调试器,或者它在不同的选项下,对同一个寄存器可能就有多种称呼。又或者你在调试器下看到的名称和书籍上的不一样。所以,又必要知道这些寄存器各自都有哪些别名。
我们运行下命令
$ arm-linux-gnueabi-objdump --help
....省略
The following ARM specific disassembler options are supported for use with
the -M switch:
reg-names-special-atpcs Select special register names used in the ATPCS
reg-names-atpcs Select register names used in the ATPCS
reg-names-apcs Select register names used in the APCS
reg-names-std Select register names used in ARM's ISA documentation
reg-names-gcc Select register names used by GCC
reg-names-raw Select raw register names
force-thumb Assume all insns are Thumb insns
no-force-thumb Examine preceeding label to determine an insn's type
我们下载它的源码打开看看
$ sudo apt-get source binutils-arm-linux-gnueabi
完成后,在下载目录下多了几个东东,其中有一个文件夹binutils-2.18.1~cvs20080103,这是debian对官方binutils进行过修改的源码。在里面搜索文件arm-dis.c,该文件中有以下这个数组。
就是不同标准下各个寄存器的不同别名。
static const arm_regname regnames[] =
{
{ "raw" , "Select raw register names",
{ "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"}},
{ "gcc", "Select register names used by GCC",
{ "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "sl", "fp", "ip", "sp", "lr", "pc" }},
{ "std", "Select register names used in ARM's ISA documentation",
{ "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r12", "sp", "lr", "pc" }},
{ "apcs", "Select register names used in the APCS",
{ "a1", "a2", "a3", "a4", "v1", "v2", "v3", "v4", "v5", "v6", "sl", "fp", "ip", "sp", "lr", "pc" }},
{ "atpcs", "Select register names used in the ATPCS",
{ "a1", "a2", "a3", "a4", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "IP", "SP", "LR", "PC" }},
{ "special-atpcs", "Select special register names used in the ATPCS",
{ "a1", "a2", "a3", "a4", "v1", "v2", "v3", "WR", "v5", "SB", "SL", "FP", "IP", "SP", "LR", "PC" }},
};
但是可以看到,该列表并没有包含AAPCS标准,AAPCS标准对 r9 又引入了一个别名 TR,这样AAPCS下,r9使用了三个别名v6, SB, TR。选用哪个
别名,是依赖于不同平台的选择。
[扩展,简要说明原理。并用实例解析]
寄存器的角色与保护
寄存器的角色(AAPCS标准)
寄存器 可选寄存器名 特殊寄存器名 在函数调用中的角色
r15 PC The Program Counter.
r14 LR The Link Register.
r13 SP The Stack Pointer.
r12 IP The Intra-Procedure-call scratch register.
r11 v8 Variable-register 8.
r10 v7 Variable-register 7.
r9 v6/SB/TR Platform register. The meaning of this register is defined by the platform standard
r8 v5 Variable-register 5.
r7 v4 Variable register 4.
r6 v3 Variable register 3.
r5 v2 Variable register 2.
r4 v1 Variable register 1.
r3 a4 Argument / scratch register 4.
r2 a3 Argument / scratch register 3.
r1 a2 Argument / result / scratch register 2.
r0 a1 Argument / result / scratch register 1.
前四个寄存器r0-r3 (a1-a4)用于传递参数给子函数或从函数中返回结果值。他们也可用于在一个函数中保存寄存器的值(但是,一般只用在子函数调用中)。
寄存器r12 (IP) 可在函数以及该函数调用的任何子函数中被链接器用作临时寄存器。它也可以在函数调用中用于保存寄存器的值。
寄存器r9的角色是平台相关的。虚拟系统可能赋予该寄存器任何角色,因此必须说明它的用法。比如,在位置无关数据模型中它可以指定为static base(SB),或者在带有本地线程存储的环境中指定它为thread register(TR)。该寄存器的使用可能要求在所有调用过程前后,它保存的值必须不变。在一个不需要这样特殊寄存器的虚拟平台上,r9可以指定为新增的callee-saved variable register,v6.
通常,寄存器r4-r8, r10 和 r11 (v1-v5, v7 和 v8)用于保存函数的本地变量。这些寄存器中,只有v1-v4能被整个thumb指令集一致地使用,但是AAPCS并没有规定Thumb代码只能使用这些寄存器。
子函数必须保护寄存器r4-r8, r10, r11 和 SP(还有r9,如果在函数调用过程中r6被指定为v6的话)的值。
在所有的函数调用标准中,寄存器r12-r15都扮演特殊的角色。依据这些角色,它们被标注为IP, SP, LR 和 PC。
寄存器CPSR的属性(省)
寄存器保护规则
子函数必须保护寄存器r4-r8, r10, r11 和 SP(还有r9,如果在函数调用过程中r6被指定为v6的话)的值。 子函数调用
子函数调用
ARM 和 Thumb 指令集都有一个函数调用指令元语,BL,它执行branch-with-link 操作。BL的执行效果是把紧跟程序计数器的下一个值--也就是返回地址--传送到链接寄存器(LR),然后把目标地址传送到程序寄存器(PC)中。如果BL指令是在Thumb状态下执行的,链接寄存器的Bit 0就设置为1;如果是在ARM状态下执行的,则设置为0。执行的结果是,把控制权转给目标地址,并把存放在LR中的返回地址作为附加的参数传递给了被调用的函数。
当返回地址装载到PC时,控制就返回给跟随BL后面的指令。
子函数调用可以由具有下面效果的任何指令序列完成:
LR[31:1] ← 返回地址
LR[0] ← 返回地址的代码类型 (0 ARM, 1 Thumb)
PC ← 子函数地址
...
返回地址:
例如,在ARM状态中,调用由r4指定了地址的子函数
do:
MOV LR, PC
BX r4
...
注意,相同的指令序列在Thumb状态中将不能工作,因为设置LR的指令并没有拷贝Thumb 状态标志位到LR[0]中。
在ARM V5架构中,ARM 和 Thumb指令集都提供了BLX指令,它将调用由一个寄存器指定了地址的子函数,并正确地设置返回地址为程序计数器的下一个值。
条件执行
操作码[31:28] 助记符扩展 解释 用于执行的标志位状态
0000 EQ 相等/等于0 Z置位
0001 NE 不等 Z清0
0010 CS/HS 进位/无符号数高于或等于 C置位
0011 CC/LO 无进位/无符号数小于 C清0
0100 MI 负数 N置位
0101 PL 正数或0 N清0
0110 VS 溢出 V置位
0111 VC 未溢出 V清0
1000 HI 无符号数高于 C置位,Z清0
1001 LS 无符号数小于或等于 C清0,Z置位
1010 GE 有符号数大于或等于 N等于V
1011 LT 有符号数小于 N不等于V
1100 GT 有符号数大于 Z清0且N等于V
1101 LE 有符号数小于或等于 Z置位且N不等于V
1110 AL 总是 任何状态
1111 NV 从不(未使用) 无
调用链的形成
注意对比ARM和X86在调用链形成的类似和区别之处。
区别,首先在寄存器的名称和角色的差异。
1. X86中寄存器%eip指向的是下一个将要执行的指令。在ARM中也有个类似别名的寄存器ip。但这个寄存器ip的作用并不是指向的是下一个将要执行的指令。在ARM中,寄存器pc才是起着X86中寄存器%eip的角色,也就是包含下一个将要执行指令的地址。而ARM中的ip寄存器,作用比较自由,类似干杂工的人,一般用于临时寄存器。[扩展,引用权威手册的话]
2. X86中,返回地址是直接保存在栈中的。但是ARM不一样了,它寄存器比X86多得多,财大气粗,所以,返回地址保存在了专用的寄存器lr(link register)中。但是,不要以为把返回地址放到专用的寄存器中会省事,其实反而多事了。因为,在调用函数刚执行完调用语句之时,lr保存的是子函数的返回地址,而指令控制权转移到了子函数后,子函数照样可能调用自己的子函数,依次需要使用lr。所以自然也就有了lr的值的保存与恢复的问题,解决方法还是要靠压栈解决。(参考下面的内容)
3. 我们知道,描述栈帧就是描述栈帧的基地址和顶端地址。在X86中,用专用的寄存器%ebp保存栈基址,也就是base pointer;%esp保存栈顶端地址,也就是stack pointer。在ARM中,也有专用的寄存器保存栈顶端地址,就是SP(stack pointer的简称)。但是,在保存栈基址这方面,依据最新的AAPCS标准,ARM就很吝啬了,没有一个保存栈基址的专用寄存器。又不过呢,在APCS和ATPCS标准中,有fp寄存器用于保存帧指针(frame pointer,也就是X86的base pointer)。在现在的编译器,可以看到,还是依照惯例把fp用于保存帧指针。既然如此,当然也有个入栈保存恢复的问题。
调用链包含两方面的内容,和X86类似
1.返回地址的保存与恢复
由调用函数在执行调用指令时把子函数的返回地址传送进连接寄存器lr中,指令控制权转交给子函数后,再由子函数负责把上层函数的lr(也就是子函数的返回地址)保存到栈中。然后子函数在返回前的最后时刻,再负责把lr的保存值从栈弹回到lr中,从而恢复了上层函数的lr。这时还没完事,子函数在执行返回指令时,由返回指令把lr的值传送到寄存器pc(Program Counter),从而导致接下来的指令是从子函数的返回地址开始运行。这样,指令控制权就返回给了调用函数。
我们应当注意到,ARM中调用指令也是多种多样的。有b,bl,bx,bxl。如果调用指令是不带连接的指令,比如b,bx,这时就要人工给lr赋值。不过为了简便,我不再区分这两类指令,而把实现跳转和连接以及可能的换态这些功能的整个指令序列为“调用指令”,相关区别参考指令手册。在ARM中,返回指令和调用指令都是同一套的。而X86,调用用call,返回用ret。
2.旧栈帧的保存与恢复
对比X86栈帧的保存与恢复的方式,ARM的更加简单直接。就是直接把上一栈帧的帧指针(frame pointer,也就是栈帧基地址)以及栈顶端指针sp(stack pointer)压入栈中。子函数返回时,在执行返回指令之前的最后关头才从栈弹出fp和sp的值,从而恢复旧栈帧。这个过程真的没有遗漏了吗?我们看下,上面的步骤保证了调用函数的栈帧不被破坏,但是子函数自己的栈帧却没有建立起来呢。首先是帧指针需要人赋值。这个情形和X86非常相似。子函数在使用栈帧之前,把上层函数的栈顶端指针sp赋给一个临时寄存器ip,然后在旧fp的值被压栈保存之后,把ip的值减去4,再赋给帧指针寄存器fp,此时,fp就指向了新栈帧的基址。这是因为,新栈帧基地址刚好位于旧栈帧栈顶之下,地址低了4字节。其次,子函数栈帧的栈顶指针sp也是要考虑的,根据压栈指令的不同,sp可能不需要人工维护,也可能需要人工维护[有疑问...????]。
我们还注意到,在X86中,子函数的栈帧的底端(也就是%ebp所指的内存位置)存放着上一层栈帧的基址指针(旧%ebp)的值,一层层下去,这样就形成回溯的链条。那么,在ARM之下,也是靠子函数的栈帧的底端提供回溯的能力的吗?当然不是。实际上子函数的栈帧的基址位置存放的是什么,这无所谓的。
[疑问???如果旧fp保存在新栈帧中的位置不是固定的,那么调试器是如何做到栈帧回溯的呢?]
根据AAPCS标准的规定,子函数必须保护寄存器r4-r8, r10, r11 和 SP(还有r9,如果在函数调用过程中r6被指定为v6的话)的值。注意,它用的字眼是“保护”,而不是“保存”。
应用层实例解析
#include
void func()
{}
void funb()
{
func();
}
void funa()
{
funb();
}
int main()
{
funa();
}
-----------
000083b0 :
#include
void func()
{}
83b0: e1a0c00d mov ip, sp
83b4: e92dd800 push {fp, ip, lr, pc}
83b8: e24cb004 sub fp, ip, #4 ; 0x4
83bc: e24bd00c sub sp, fp, #12 ; 0xc
83c0: e89d6800 ldm sp, {fp, sp, lr}
83c4: e12fff1e bx lr
000083c8 :
void funb()
{
83c8: e1a0c00d mov ip, sp
83cc: e92dd800 push {fp, ip, lr, pc}
83d0: e24cb004 sub fp, ip, #4 ; 0x4
func();
83d4: ebfffff5 bl 83b0
}
83d8: e24bd00c sub sp, fp, #12 ; 0xc
83dc: e89d6800 ldm sp, {fp, sp, lr}
83e0: e12fff1e bx lr
000083e4 :
void funa()
{
83e4: e1a0c00d mov ip, sp
83e8: e92dd800 push {fp, ip, lr, pc}
83ec: e24cb004 sub fp, ip, #4 ; 0x4
funb();
83f0: ebfffff4 bl 83c8
}
83f4: e24bd00c sub sp, fp, #12 ; 0xc
83f8: e89d6800 ldm sp, {fp, sp, lr}
83fc: e12fff1e bx lr
00008400 :
int main()
{
8400: e1a0c00d mov ip, sp
8404: e92dd800 push {fp, ip, lr, pc}
8408: e24cb004 sub fp, ip, #4 ; 0x4
funa();
840c: ebfffff4 bl 83e4
}
8410: e24bd00c sub sp, fp, #12 ; 0xc
8414: e89d6800 ldm sp, {fp, sp, lr}
8418: e12fff1e bx lr
内核层实例解析
栈帧结构与参数传递
[1.栈:栈对齐,栈限制。2.参数传递:variadic函数,nonvariadic函数。3.结果的返回 4.互交代码(ARM-Thumb interworking)]
栈帧示意图
+------------------------------ + ---------
| Register Save Area | |
+------------------------------ + |
| Locals and Temporaries | |
+------------------------------ +
| alloca() Locals | Caller's Frame
+------------------------------ +
| Incoming Args Past Four Words | |
+------------------------------ + ---------
| First Four Words Of Args | |
Frame Pointer--> +------------------------------ + |
| Register Save Area | |
+------------------------------ + Current Frame
| Locals and Temporaries |
+------------------------------ + |
| alloca() Locals | |
+------------------------------ + |
| Outgoing Args Past Four Words | |
Stack Pointer---> +------------------------------ + ---------
完整的调用过程
函数caller调用子函数callee,这是应用层的普通函数调用过程。如果是远调用,跨态调用要考虑的东西更多。但这个例子已经充分展示了调用过程的繁复部分。
函数调用前调用者的动作
函数调用 call callee
函数调用后被调用者的动作
调用返回前被调用者的动作
调用返回后调用者的动作
应用层实例解析
内核层实例解析
调用链回溯的实现
arm体系对调用链的回溯的代码实现主要在
arch/arm/kernel/traps.c 和arch/arm/lib/backtrace.S.其中核心函数是backtrace.S中的__backtrace函数。
待解释
---/*
* linux/arch/arm/lib/backtrace.S
*
* Copyright (C) 1995, 1996 Russell King
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* 27/03/03 Ian Molton Clean up CONFIG_CPU
*
*/
#include
#include
.text
@ fp is 0 or stack frame
#define frame r4
#define sv_fp r5
#define sv_pc r6
#define mask r7
#define offset r8
ENTRY(__backtrace)
mov r1, #0x10
mov r0, fp
ENTRY(c_backtrace)
#if !defined(CONFIG_FRAME_POINTER) || !defined(CONFIG_PRINTK)
mov pc, lr
ENDPROC(__backtrace)
ENDPROC(c_backtrace)
#else
stmfd sp!, {r4 - r8, lr} @ Save an extra register so we have a location...
movs frame, r0 @ if frame pointer is zero
beq no_frame @ we have no stack frames
tst r1, #0x10 @ 26 or 32-bit mode?
moveq mask, #0xfc000003 @ mask for 26-bit
movne mask, #0 @ mask for 32-bit
1: stmfd sp!, {pc} @ calculate offset of PC stored
ldr r0, [sp], #4 @ by stmfd for this CPU
adr r1, 1b
sub offset, r0, r1
/*
* Stack frame layout:
* optionally saved caller registers (r4 - r10)
* saved fp
* saved sp
* saved lr
* frame => saved pc
* optionally saved arguments (r0 - r3)
* saved sp =>
*
* Functions start with the following code sequence:
* mov ip, sp
* stmfd sp!, {r0 - r3} (optional)
* corrected pc => stmfd sp!, {..., fp, ip, lr, pc}
*/
for_each_frame: tst frame, mask @ Check for address exceptions
bne no_frame
1001: ldr sv_pc, [frame, #0] @ get saved pc
1002: ldr sv_fp, [frame, #-12] @ get saved fp
sub sv_pc, sv_pc, offset @ Correct PC for prefetching
bic sv_pc, sv_pc, mask @ mask PC/LR for the mode
1003: ldr r2, [sv_pc, #-4] @ if stmfd sp!, {args} exists,
ldr r3, .Ldsi+4 @ adjust saved 'pc' back one
teq r3, r2, lsr #10 @ instruction
subne r0, sv_pc, #4 @ allow for mov
subeq r0, sv_pc, #8 @ allow for mov + stmia
ldr r1, [frame, #-4] @ get saved lr
mov r2, frame
bic r1, r1, mask @ mask PC/LR for the mode
bl dump_backtrace_entry
ldr r1, [sv_pc, #-4] @ if stmfd sp!, {args} exists,
ldr r3, .Ldsi+4
teq r3, r1, lsr #10
ldreq r0, [frame, #-8] @ get sp
subeq r0, r0, #4 @ point at the last arg
bleq .Ldumpstm @ dump saved registers
1004: ldr r1, [sv_pc, #0] @ if stmfd sp!, {..., fp, ip, lr, pc}
ldr r3, .Ldsi @ instruction exists,
teq r3, r1, lsr #10
subeq r0, frame, #16
bleq .Ldumpstm @ dump saved registers
teq sv_fp, #0 @ zero saved fp means
beq no_frame @ no further frames
cmp sv_fp, frame @ next frame must be
mov frame, sv_fp @ above the current frame
bhi for_each_frame
1006: adr r0, .Lbad
mov r1, frame
bl printk
no_frame: ldmfd sp!, {r4 - r8, pc}
ENDPROC(__backtrace)
ENDPROC(c_backtrace)
.section __ex_table,"a"
.align 3
.long 1001b, 1006b
.long 1002b, 1006b
.long 1003b, 1006b
.long 1004b, 1006b
.previous
#define instr r4
#define reg r5
#define stack r6
.Ldumpstm: stmfd sp!, {instr, reg, stack, r7, lr}
mov stack, r0
mov instr, r1
mov reg, #10
mov r7, #0
1: mov r3, #1
tst instr, r3, lsl reg
beq 2f
add r7, r7, #1
teq r7, #6
moveq r7, #1
moveq r1, #'/n'
movne r1, #' '
ldr r3, [stack], #-4
mov r2, reg
adr r0, .Lfp
bl printk
2: subs reg, reg, #1
bpl 1b
teq r7, #0
adrne r0, .Lcr
blne printk
ldmfd sp!, {instr, reg, stack, r7, pc}
.Lfp: .asciz "%cr%d:%08x"
.Lcr: .asciz "/n"
.Lbad: .asciz "Backtrace aborted due to bad frame pointer <%p>/n"
.align
.Ldsi: .word 0xe92dd800 >> 10 @ stmfd sp!, {... fp, ip, lr, pc}
.word 0xe92d0000 >> 10 @ stmfd sp!, {}
#endif
源码浏览工具
本节意义: 内核源码的代码量越来越大,不借助源码交叉索引工具根本是无法阅读了。一定要熟练灵活掌握此类工具的使用
调用图生成工具
1.CodeViz
官网:
http://www.csn.ul.ie/~mel/projects/codeviz/
安装使用:
CodeViz —— 一款分析C_C++源代码中函数调用关系的调用图生成工具.pdf
http://linux.chinaunix.net/bbs/thread-1031921-1-1.html
用CodeViz产生函数调用图
http://barry-popy.blog.sohu.com/31629163.html
分析函数调用关系图(call graph)的几种方法
http://blog.csdn.net/Solstice/archive/2005/09/24/488865.aspx
用CodeViz绘制函数调用关系图(call graph)
http://blog.csdn.net/Solstice/archive/2005/09/22/486788.aspx
2.ncc
find + grep
对于源码的阅读工具,一般是选取后面提到的某种源码索引工具,再和find以及grep“高低搭配”一起来使用。
1.命令选项
2.正则表达式
Regular Expression HOWTO: http://www.amk.ca/python/howto/regex/
正则表达式之道: http://net.pku.edu.cn/~yhf/tao_regexps_zh.html
wine + SI
wine + source insight
优缺点
优点: SI的特点是有图形界面,操作和浏览特别方便快捷。特别是它的“函数调用树”的图形显示功能,以及分窗口自动显示函数,变量等定义的功能。
缺点: 不能解析汇编源文件。
安装wine
在ubuntu/debian下用以下命令就能在线安装wine
$ sudo apt-get install wine
安装好后,就能看到wine的快捷菜单被添加到了任务栏的“应用程序”中。
安装SI
wine安装好后,就可以像在windows一样去安装使用SI了。安装完成后,SI的快捷菜单被添加到“应用程序”→“wine”→“programs”→“source insight3”中。以后用快捷菜单就能启动SI
SI的使用
可以乱点乱试一下,它能提供很多的功能。其中一些经常要到的功能有 查找符号;函数调用的函数,被调用的函数;以及调用关系的多层展开显示;字符串搜索等。
global
[待玩] http://www.gnu.org/software/global/
Source-Navigator
[待玩] http://sourcenav.sourceforge.net/
安装:
在ubuntu下可以在线安装
$ sudo apt-get install sourcenav
运行:
$ snavigator
vim + cscope/ctags
参考:
cscope的官方教程 “The Vim/Cscope tutorial”:
http://cscope.sourceforge.net/cscope_vim_tutorial.html
对应的中文翻译: http://www.gracecode.com/Archive/Display/316
http://www.lupaworld.com/?uid-151392-action-viewspace-itemid-106656
http://dev.21tx.com/2007/02/21/10252.html
优缺点
优点: 本人感觉在终端下看源码比较舒服。
缺点: 没有一个实时显示函数/变量定义的分窗口。也不能直接显示“调用树”,但有其他小工具可以实现该功能。也许vim高手能解决这些问题。
安装cscope/ctags
ubuntu/debian下用以下命令就能在线安装
$ sudo apt-get install cscope ctags
命令选项
在终端下可以用 man info –help等形式查看cscope/ctags的手册
在vim下查看手册的方式是
:help cscope
和
:help ctags
1. 以下是cscope建立索引文件用到的一些选项
-R: 在生成索引文件时,搜索子目录树中的代码
-b: 只生成索引文件,不进入cscope的界面
-q: 生成cscope.in.out和cscope.po.out文件,加快cscope的索引速度
-k: 在生成索引文件时,不搜索/usr/include目录
-i: 如果保存文件列表的文件名不是cscope.files时,需要加此选项告诉cscope到哪儿去找源文件列表。可以使用“-”,表示由标准输入获得文件列表。
-I dir: 在-I选项指出的目录中查找头文件
-u: 扫描所有文件,重新生成交叉索引文件
-C: 在搜索时忽略大小写
-P path: 在以相对路径表示的文件前加上的path,这样,你不用切换到你数据库文件所在的目录也可以使用它了。
2. 在vim下利用:cscope find <关键字> 命令的选项有
s: 查找C语言符号,即查找函数名、宏、枚举值等出现的地方
g: 查找函数、宏、枚举等定义的位置,类似ctags所提供的功能
d: 查找本函数调用的函数
c: 查找调用本函数的函数
t: 查找指定的字符串
e: 查找egrep模式,相当于egrep功能,但查找速度快多了
f: 查找并打开文件,类似vim的find功能
i: 查找包含本文件的文
使用
建立索引
[可能要修改]
用以下命令先产生一个文件列表,然后让cscope为这个列表中的每个文件都生成索引。在这里,我们只关注.h, .c, .S文件,所以只对他们进行索引。可以根据自己需求进行更改。接着我们用-bq选项利用cscope生成索引。选项意义见上节。同时也生成ctags索引。
#!/bin/sh
find . -name "*.h" -o -name "*.c" -o -name "*.S" > cscope.files
cscope -bkq -i cscope.files
ctags -R
利用vim浏览源码
切换到内核源码的目录上,运行vim,然后在vim下导入索引
$vim
:cscope add cscope.out
然后就可以在vim下调用“:cscope find <关键字>”来查找函数的定义,函数调用的函数以及被调用函数等
“:cscope find <关键字>” 可以缩写为 “:cs f <关键字>”
比如以下命令用来查找sys_read的定义
:cs f g sys_read
“cs f”的其他命令选项请看上节
快捷键的使用
ctrl + t : 退回
ctrl + ] : 进入光标处的变量/函数的定义处
kscope
kscope是cscope的图形前端工具。在ubuntu下可以在线安装。它的界面上和操作上与source insight都比较类似。但是目前它对cpu的占用很大,不是很好。但是它和cscope相比,有一个很大的优点是:可以图形显示“函数调用树”,甚至这个功能比SI还强大。
$sudo apt-get install kscope
lxr
1. 优缺点
优点:本身好像没什么特别的优点。但是有专门提供这种服务的网站,上面有很多不同系统的不同版本源码
缺点:在本机上配置运行的话,配置麻烦。如果是浏览lxr站点的方式,速度比较慢。
2. lxr官方: http://lxr.linux.no/
特点是可以浏览历史上linux所有版本的源码,可以看到它的演化过程。
3. 其他系统的源码 http://fxr.watson.org/
估计超一流的内核开发人员,可能会经常访问此类站点。因为他需要借鉴其他系统的设计思想。
SI等与gdb的特点
在源码阅读的功能上:
1. SI等适合“面读”,也就是读一个代码段,并且提供更舒适的阅读辅助手段。SI适合分析函数全面的逻辑。
2. gdb适合“线读”,也就是以追踪调用链的方式深入阅读,并且提供了数据分析的调试功能。适合分析特定情况下的函数逻辑表现。
调用链、调用树和调用图
为了能使用调试器,必须理解函数调用链在调试器级别的表现形式。但是,因为存在内嵌函数和代码优化等原因,调试器的表现形式和源码浏览器下的表现形式是不一样的。它们两者的信息显示可能存在“错位”的现象。本节的目的就是为了磨合调试器和交叉索引工具之间的代沟。
为了简化问题的描述,在实际分析前,先将知识点分解介绍一下。
理想调用链
下面我给出一个处于“理想状态”的经典backtrace(backtrace的意思是“回溯”,依照它的作用来说,也就是本人说的调用链)。所谓“理想状态的”的backtrace是指,可以利用内核源码交叉索引工具,依据gdb给出的这个backtrace,从frame 0开始一级级往后最追溯,能够一直追溯到最前面的frame N,而且追溯的过程中,没有出现多出来的连接frameN和frame(N-1)的“过渡”frame.
注意其中的两个条件:1.能够 2.不多出。但是,在现实的世界里,往往没这么美好。源码浏览工具往往要么“不能”,要么“多出”。造成前者的原因在于源码浏览工具的局限性,造成后者的是内嵌函数以及代码优化。详细情况可看下节的分析。
追溯的方法对于source insight来说就是:打开”relation window”→选中要被追溯的函数→右键→选“view relation”→选“referenced by functions”,这样就能显示出调用了被选函数的函数来。
我们拿下面这个“理想状态”的backtrace分析一下
(gdb) bt
#0 kref_init (kref=0xdc40abe4) at lib/kref.c:33
#1 0xc01de8be in kobject_init_internal (kobj=0xdc40abe0) at lib/kobject.c:149
#2 0xc01de928 in kobject_init (kobj=0xdc40abe0, ktype=0xc035b9dc) at lib/kobject.c:282
#3 0xc01de972 in kobject_create () at lib/kobject.c:619
#4 0xc01def53 in kobject_create_and_add (name=0xdc40abe4 "", parent=0xc035b9dc) at lib/kobject.c:641
#5 0xc0393b04 in mnt_init () at fs/namespace.c:2333
#6 0xc039382b in vfs_caches_init (mempages=108676) at fs/dcache.c:2212
#7 0xc037f868 in start_kernel () at init/main.c:666
#8 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#9 0x00000000 in ?? ()
理想状态下的backtrace各个域的含义是(注意,在非理想状态的backtrace中,这些含义往往对不上号)
#frameN的编号 frame(N-1)的返回地址(注:fram0没有这项) in frameN所处的函数(该函数的参数...) at 该函数所处的源文件 : frameN函数内对frame(N-1)函数的调用语句在源文件中所处的行数
我们看下
#0 kref_init (kref=0xdc40abe4) at lib/kref.c:33
它说明frame0时,kref_init正要运行。传入的参数是0xdc40abe4。函数kref_init从源文件lib/kref.c第33行开始。 在gdb下调用shell来查看源文件
(gdb) shell vi lib/kref.c
vi 出来后打命令:set nu可看到
31 */
32 void kref_init(struct kref *kref)
33 {
34 kref_set(kref, 1);
35 }
36
我们再看看frame0这一瞬间是不是“kref_init正要运行”。应该知道,“正要运行”和“正要被调用”是两个不同的概念。前者来说,到了下一个指令,代码的控制权就会交给了被调用的函数;而后者,到了下一个指令,代码的控制权还在调用者手里,
(gdb) f 0
#0 kref_init (kref=0xdc40abe4) at lib/kref.c:33
33 {
(gdb) info registers
....
edi 0x0 0
eip 0xc01df520 0xc01df520 //<-注意eip是下一个将要运行的指令地址
eflags 0x282 [ SF IF ]
....
(gdb) disass kref_init
Dump of assembler code for function kref_init:
0xc01df520 : push %ebp //对比上面,eip指向这里
0xc01df521 : mov %esp,%ebp
...
0xc01df52f : ret
End of assembler dump.
(gdb)
可见,kobject_init_internal的调用指令call已经执行完毕,到了frame0时,下一个指令“将要运行”函数kref_init。
再看看
#1 0xc01de8be in kobject_init_internal (kobj=0xdc40abe0) at lib/kobject.c:149
frameN与frame(N-1)之间是调用的关系,前者调用了后者。也就是说,frame1的kobject_init_internal调用frame0的kref_init,并且kref_init函数返回后,将返回到地址0xc01de8be继续执行。0xc01de8be就在kobject_init_internal的体内,函数kobject_init_internal中调用kref_init的C语句位于lib/kobject.c的149行。
查看一下kobject_init_internal的反汇编码
(gdb) disass kobject_init_internal
Dump of assembler code for function kobject_init_internal:
0xc01de8ac : push %ebp
0xc01de8ad : test %eax,%eax
0xc01de8af : mov %esp,%ebp
0xc01de8b1 : push %ebx
0xc01de8b2 : mov %eax,%ebx
0xc01de8b4 : je 0xc01de8d3
0xc01de8b6 : lea 0x4(%eax),%eax
0xc01de8b9 : call 0xc01df520
0xc01de8be : lea 0x8(%ebx),%eax //注意这个地址0xc01de8be是kref_init的返回地址
0xc01de8c1 : mov %eax,0x8(%ebx)
再看看lib/kobject.c,看看最后的那个行数的意义
145 static void kobject_init_internal(struct kobject *kobj)
146 {
147 if (!kobj)
148 return;
149 kref_init(&kobj->kref); //注意kobject_init_internal调用子函数kref_init的C语句位于行数149
150 INIT_LIST_HEAD(&kobj->entry);
151 kobj->state_in_sysfs = 0;
152 kobj->state_add_uevent_sent = 0;
153 kobj->state_remove_uevent_sent = 0;
154 kobj->state_initialized = 1;
155 }
在验证一下
#2 0xc01de928 in kobject_init (kobj=0xdc40abe0, ktype=0xc035b9dc) at lib/kobject.c:282
看看kobject_init的反汇编码
(gdb) disass kobject_init
Dump of assembler code for function kobject_init:
0xc01de8f3 : push %ebp
........
0xc01de923 : call 0xc01de8ac
0xc01de928 : mov %esi,0x18(%ebx) //注意这个地址0xc01de928是kobject_init_internal的返回地址
......
0xc01de94b : pop %ebp
0xc01de94c : ret
End of assembler dump.
看看看看lib/kobject.c,看看最后的那个行数的意义
263 void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
264 {
265 char *err_str;
.......
282 kobject_init_internal(kobj); ////注意kobject_init调用子函数kobject_init_internal的C语句位于行数282
283 kobj->ktype = ktype;
......
287 printk(KERN_ERR "kobject (%p): %s/n", kobj, err_str);
288 dump_stack
通过这两个例子,可见最初的猜想是正确的。
函数指针调用
本小节意义: 在利用SI等工具查看函数调用链时,遇到的一个最多的问题是函数指针的调用。所以把该小节内容移到这里来,为下小节的叙述作铺垫。SI等交叉索引工具不能在父函数内部解析出这种调用关系。
我们经常碰到这种情况:如果内核中函数A是通过函数指针调用函数B,那么源码交叉索引工具(如source insight, kscope等)就无法通过函数B的名称回溯到上层函数A。这是因为在函数A内部对函数B的调用并不是通过函数B的名称,而是利用指向函数B代码块的指针(函数指针)。
要想解决这个问题,方法有两种:
1. 利用字符串搜索功能:
搜索函数指针的变量名。如果已经知道的是子函数,想找出通过指针调用它的所有上层父函数:利用子函数的函数名进行搜索,就能找到所有相应的函数指针变量赋值的语句。然后搜索该函数指针变量就能得到所有可能调用该函数的上层父函数。相反,如果是已经知道父函数,想知道该父函数体内的一个函数指针可能会调用哪些子函数,可以搜索该函数指针变量(一般在该变量名前加个点号“.”),这样可以搜索出所有给该函数指针变量赋值的语句,从而找出所有可能的子函数。
当然,既然是字符串搜索,搜索结果中会夹带其他没用的信息,这需要进一步的筛选。这个方法能搜索出依赖某函数指针变量的所有调用关系。
2. 利用调试工具:
在目标函数处下断点。调试器器会实时拦截该函数的调用,然后用bt命令就能看到整个调用链。
这个方法得到的只是一个特定的具体调用关系。可能还有其他很多的潜在调用路径。
然而,我们研究的目标并不满足于知道调用链。下面我们观察函数究竟是怎样利用函数指针调用子函数的。[待整理]
2130 int vfs_mkdir(struct inode *dir, struct dentry *dentry, int mode)
2131 {
2132 int error = may_create(dir, dentry, NULL);
2133
2134 if (error)
2135 return error;
2136
2137 if (!dir->i_op || !dir->i_op->mkdir)
2138 return -EPERM;
2139
2140 mode &= (S_IRWXUGO|S_ISVTX);
2141 error = security_inode_mkdir(dir, dentry, mode);
2142 if (error)
2143 return error;
2144
2145 DQUOT_INIT(dir);
2146 error = dir->i_op->mkdir(dir, dentry, mode);
2147 if (!error)
2148 fsnotify_mkdir(dir, dentry);
2149 return error;
2150 }
对源码文件下断点
(gdb) b fs/namei.c:2146
Breakpoint 9 at 0xc017c0ee: file fs/namei.c, line 2146.
问题一:
动态分析call *0x14(%ebx)是怎么回事,函数指针
-------------------------
┌──Register group: general───────────────────────────────────────────────────────────────────────────────────────────────────┐
│eax 0xdc20b0a8 -601837400 ecx 0x1ed 493 │
│edx 0xdb9526c0 -610982208 ebx 0xe01c87d4 -535001132 │
│esp 0xd8c5bf1c 0xd8c5bf1c ebp 0xd8c5bf34 0xd8c5bf34 │
│esi 0xdc20b0a8 -601837400 edi 0xdb9526c0 -610982208 │
│eip 0xc017c0fb 0xc017c0fb eflags 0x200246 [ PF ZF IF ID ] │
│cs 0x60 96 ss 0x68 104 │
│ds 0x7b 123 es 0x7b 123 │
│fs 0xd8 216 gs 0x33 51 │
│ │
│ │
│ │
│ │
│ │
│ │
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│0xc017c0ea mov %esi,%eax │
│0xc017c0ec call *(%ecx) │
B+ │0xc017c0ee mov 0x98(%esi),%ebx │
│0xc017c0f4 mov %edi,%edx │
│0xc017c0f6 mov %esi,%eax │
│0xc017c0f8 mov -0x10(%ebp),%ecx │
>│0xc017c0fb call *0x14(%ebx) │
│0xc017c0fe test %eax,%eax │
│0xc017c100 mov %eax,%ebx │
│0xc017c102 jne 0xc017c15d │
│0xc017c104 testb $0x4,0x11c(%esi) │
│0xc017c10b je 0xc017c119 │
│0xc017c10d mov $0x4,%edx │
│0xc017c112 mov %esi,%eax │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: vfs_mkdir Line: 2146 PC: 0xc017c0fb
i_state = 1,
dirtied_when = 0,
i_flags = 0,
i_writecount = {
counter = 0
},
i_security = 0x0,
i_private = 0x0
}
(gdb) p/x $ebx
$20 = 0xe01c87d4
(gdb) p/x $ebx+0x14
$21 = 0xe01c87e8
(gdb) p &sfs_dir_inode_ops
$13 = (struct inode_operations *) 0xe01c87d4
(gdb) p/x *(int * )0xe01c87d4@10
$18 = {0xe01c75b1, 0xe01c7677, 0xc018d3f0, 0xc018cc91, 0xe01c75dd, 0xe01c75c0, 0xc018d441, 0xe01c7510, 0xc018d474, 0x0}
(gdb) disass sfs_mkdir
Dump of assembler code for function sfs_mkdir:
0xe01c75c0 : push %ebp //<-
0xe01c75c1 : or $0x40,%ch
0xe01c75c4 : mov %esp,%ebp
0xe01c75c6 : push %ebx
0xe01c75c7 : mov %eax,%ebx
0xe01c75c9 : push $0x0
0xe01c75cb : call 0xe01c7510
0xe01c75d0 : pop %edx
0xe01c75d1 : test %eax,%eax
0xe01c75d3 : jne 0xe01c75d8
0xe01c75d5 : incl 0x28(%ebx)
0xe01c75d8 : mov -0x4(%ebp),%ebx
0xe01c75db : leave
0xe01c75dc : ret
End of assembler dump.
(gdb) p/x *0xe01c87e8
$9 = 0xe01c75c0 // <-sfs_mkdir的地址
(gdb)
struct inode_operations {
int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
int (*symlink) (struct inode *,struct dentry *,const char *);
int (*mkdir) (struct inode *,struct dentry *,int);
......
};
struct inode_operations sfs_dir_inode_ops = {
...
.mkdir = sfs_mkdir,
...
};
-----------------------------------------------------------------------
0xc017c0fb call *0x14(%ebx) 为什么要加 * ?
call *0x14(%ebx) ==
push %eip
mov 0x14(%ebx) %eip
注意call与mov指令语义的区别
mov 0x14(%ebx) %eax; 把存放在地址0x14(%ebx)中的32位数据拷贝到%eax
mov %eax 0x14(%ebx); 把%eax的值拷贝到地址0x14(%ebx)指向的内存中
call 0x14(%ebx) : 结果是跳到地址0x14(%ebx)继续执行(当然对于本例来说,该地址指向的并不是目标代码段)
call *0x14(%ebx) : 取出存放在地址0x14(%ebx)中的32位数据,把该数据作为目标地址,跳到该地址继续执行。
mov $0xe01c75c9 %eax ; 0xe01c75c9被认为是立即数,前面有$。没有mov 0xe01c75c9 %eax这种形式
call 0xe01c75c9 ;0xe01c75c9被认为是地址。没有call $0xe01c75c9这种形式。
注意,也没有call %eax等形式(假设%eax放着目标地址)。需用 call *%eax,同样,*%eax表示从%eax获取地址值
| - | | -- |
| - | | -- | 4. call sfs_mkdir == call 0xe01c75c0
| - | | -- |
+--------------------+ <-------> | -- | 3. 0xe01c75c0 == fetch from 0xe01c87e8
| --- | | -- | *0x14(%ebx)
+--------------------+ +---------------+
| init (*mkdir)(..) +--+ | 0xe01c75c0 | 2. 0xe01c87e8 == calculate 0x14(%ebx)
0x14(%ebx)---> +--------------------+ | +---------------+
| ... | | | 0xe01c75dd |
+--------------------+ | +---------------+
| ... | | | 0xc018cc91 |
+--------------------+ | +---------------+
| ... | | | 0xc018d3f0 |
+--------------------+ | +---------------+ 1. 0xe01c87d4 == fetch from %ebx
| ... | | | 0xe01c7677 |
+--------------------+ | +---------------+ +------------+
| int (*create)(..) | | | 0xe01c75b1 | 0xe01c87d4 | 0xe01c87d4 |
%ebx-------> +--------------------+ | +---------------+ +------------+
struct inode_operations | contents address register %ebx
sfs_dir_inode_ops |
| call *0x14(%ebx) 的过程
|
+-----------------------------+
|
static int sfs_mkdir(..) |
0xe01c75c0 : +-> push %ebp
0xe01c75c1 : or $0x40,%ch
0xe01c75c4 : mov %esp,%ebp
0xe01c75c6 : push %ebx
0xe01c75c7 : mov %eax,%ebx
0xe01c75c9 : push $0x0
0xe01c75cb : call 0xe01c7510
0xe01c75d0 : pop %edx
0xe01c75d1 : test %eax,%eax
0xe01c75d3 : jne 0xe01c75d8
0xe01c75d5 : incl 0x28(%ebx)
0xe01c75d8 : mov -0x4(%ebp),%ebx
0xe01c75db : leave
0xe01c75dc : ret
address contents
---------------------------------------------------------------------------
问题二:
下面的dir->i_op->mkdir(),为什么不是dir.i_op.mkidr. . 和 -> 有什么区别
static int sfs_mkdir(struct inode * dir, struct dentry * dentry, int mode)
{
....
}
2130 int vfs_mkdir(struct inode *dir, struct dentry *dentry, int mode)
2131 {
....
2146 error = dir->i_op->mkdir(dir, dentry, mode);
...
2150 }
struct inode {
...
const struct inode_operations *i_op;
...
};
struct inode_operations {
...
int (*mkdir) (struct inode *,struct dentry *,int);
...
};
dir: 取得(struct inode *)dir
dir->i_op: 取得(const struct inode_operations *)i_op
dir->i_op->mkdir: 取得(int (*) (struct inode *,struct dentry *,int))mkdir
dir->i_op->mkdir(dir, dentry, mode)也就是 函数指针变量名(参数...)
函数指针是一个指针,它向目标函数的代码块的第一个指令。
函数名的值等于该函数第一条指令的地址。
(gdb) p sfs_mkdir
$20 = {int (struct inode *, struct dentry *, int)} 0xe01c75c0
(gdb) p &sfs_mkdir
$21 = (int (*)(struct inode *, struct dentry *, int)) 0xe01c75c0
(gdb) p dir->i_op->mkdir
$18 = (int (*)(struct inode *, struct dentry *, int)) 0xe01c75c0
前者指明变量名/函数名的类型,后者是它的值
struct inode_operations sfs_dir_inode_ops = {
...
.mkdir = sfs_mkdir,
...
};
函数的两种调用形式: 函数指针变量名(参数...) 函数名(参数...)
严格地说,从C语言的形式看来,前者通过函数指针变量名调用函数,后者通过函数名调用,是不同的。
但从汇编级代码看来,都是转化为指令call 函数地址。是一样的。
引入了函数指针变量后,这个变量就可以动态地赋值,从而指向不同的函数体,实现某些特殊的功能。
我们再看下函数指针的赋值.mkdir = sfs_mkdir,
严格地说,mkdir和sfs_mkdir是类型不同的东西,但在编译时自动经过了类型转换。所以下面这些写法效果都一样
.mkdir = sfs_mkdir,
.mkdir = &sfs_mkdir,
.mkdir = (int (*)(struct inode *, struct dentry *, int))sfs_mkdir,
.mkdir = (int (*)(struct inode *, struct dentry *, int))(&sfs_mkdir),
---
例子
#include
int main()
{
int myfunc(int a)
{
printf("%d/n", a);
return 0;
}
int (*funa)(int) = myfunc;
int (*funb)(int) = &myfunc;
int (*func)(int) = (int (*)(int))myfunc;
int (*fund)(int) = (int (*)(int))(&myfunc);
myfunc(1);
funa(2);
funb(3);
func(4);
fund(5);
return 0;
}
调用链的层次
1. 人观念层次
2. 交叉解析器层次
2. c调用层次
3. 编译器(机器码静态)层次
4. 运行时(机器码动态)层次,也叫调试器层次
很明显,前面所讲的“理想状态”的backtrace就是指在交叉解析器层次下和在调试器层次下的表现相同的调用链。
非理想调用链
任务:
从一个断点开始,从后向前推导,分析出ramfs注册函数的调用过程。同时,观察调试器的优点和局限性。
ramfs文件系统的注册函数是register_filesystem(&ramfs_fs_type)。为了更快定位,在上层函数init_ramfs_fs下断点。而后在gdb下得到的调用链是
(gdb) bt
#0 register_filesystem (fs=0xc03595cc) at fs/filesystems.c:68
#1 0xc0394594 in init_ramfs_fs () at fs/ramfs/inode.c:213
#2 0xc037f473 in kernel_init (unused=) at init/main.c:708
#3 0xc010463f in kernel_thread_helper () at arch/x86/kernel/entry_32.S:1013
我们注意到:
1. 这个backtrace包含的函数只有4个,实际上并非如此。经过分析,它实际上(用C的观点看)调用链如下所示,这是为什么呢?
start_kernel→rest_init→kernel_thread→kernel_thread_helper→call %ebx (即call kernel_init)→do_basic_setup→do_initcalls→do_one_initcall→result = fn() (即call init_ramfs_fs)→register_filesystem
2. backtrace推溯到kernel_thread_helper后就再没下文了。又是什么使得调试器变成了瞎子,无法看得再远了呢?
欲见其详,且听下回分解
[下面准备材料]
kernel_init对do_basic_setup的调用被优化成内联函数
do_basic_setup对do_initcalls的调用被优化成内联函数
do_initcalls对do_one_initcall的调用被优化成内联函数
有三层的非内联函数都被被优化成内联函数,整个代码被优化的乱七八糟。
838 static int __init kernel_init(void * unused)
839 {
.....
864 cpuset_init_smp();
865
866 do_basic_setup();
867
.......
887 return 0;
888 }
static void __init do_basic_setup(void)
{
/* drivers will send hotplug events */
init_workqueues();
usermodehelper_init();
driver_init();
init_irq_proc();
do_initcalls();
}
741 static void __init do_initcalls(void)
742 {
743 initcall_t *call;
744
745 for (call = __initcall_start; call < __initcall_end; call++)
746 do_one_initcall(*call);
747
748 /* Make sure there is no pending stuff from the initcall sequence */
749 flush_scheduled_work();
750 }
static void __init do_one_initcall(initcall_t fn)
{
int count = preempt_count();
ktime_t t0, t1, delta;
char msgbuf[64];
int result;
if (initcall_debug) {
print_fn_descriptor_symbol("calling %s/n", fn);
t0 = ktime_get();
}
result = fn();
if (initcall_debug) {
....
}
static inline void print_fn_descriptor_symbol(const char *fmt, void *addr)
{
#if defined(CONFIG_IA64) || defined(CONFIG_PPC64)
addr = *(void **)addr;
#endif
print_symbol(fmt, (unsigned long)addr);
}
(gdb) disass kernel_init
Dump of assembler code for function kernel_init:
0xc037f349 : push %ebp
0xc037f34a : mov %esp,%ebp
0xc037f34c : push %edi
0xc037f34d : push %esi
......
0xc037f413 : call 0xc0391454
0xc037f418 : call 0xc0390081 //<-do_basic_setup被优化成内联函数,在这里开始展开
0xc037f41d : call 0xc039004e
0xc037f422 : call 0xc039b7d1
0xc037f427 : call 0xc0153e18
0xc037f42c : movl $0xc03aa470,-0x5c(%ebp) //do_initcalls被优化成内联函数,在这里开始展开
0xc037f433 : pop %eax
0xc037f434 : pop %edx
0xc037f435 : jmp 0xc037f559
0xc037f43a : mov -0x5c(%ebp),%eax //do_one_initcall被优化成内联函数,在这里开始展开
0xc037f43d : mov (%eax),%eax
0xc037f43f : mov %eax,-0x58(%ebp)
0xc037f442 : mov %esp,%eax
0xc037f444 : and $0xffffe000,%eax
0xc037f449 : mov 0x14(%eax),%eax
0xc037f44c : cmpl $0x0,0xc03a1820
0xc037f453 : mov %eax,-0x54(%ebp)
0xc037f456 : je 0xc037f470
0xc037f458 : mov -0x58(%ebp),%edx //内联函数print_fn_descriptor_symbol在这里开始展开
0xc037f45b : mov $0xc030d1be,%eax
0xc037f460 : call 0xc013f598 <__print_symbol>//内联函数print_fn_descriptor_symbo的展开结束
0xc037f465 : call 0xc013352f
0xc037f46a : mov %eax,-0x64(%ebp)
0xc037f46d : mov %edx,-0x60(%ebp)
0xc037f470 : call *-0x58(%ebp) //do_one_initcall中的调用语句result = fn();
.....
0xc037f553 : pop %edi
0xc037f554 : pop %eax
0xc037f555 : addl $0x4,-0x5c(%ebp)
0xc037f559 : cmpl $0xc03aa804,-0x5c(%ebp) //
0xc037f560 : jb 0xc037f43a //
如何在汇编码中定位内联(或被优化掉的非内联)函数
1.利用前后相关函数的提示
2.函数的前戏码定位函数的开始
3.注意跳转语句
4.利用调试器辅助定位(见gdb技巧)
调用树与调用图
[待充实]
调用树的定义
一个复杂的函数调用一定是调用了多个子函数,同时这些子函数又会调用若干“孙”函数,这样依次调用并依次返回到最初的父函数后,就形成了树状的调用关系,我们称之为“调用树”。
调用树的作用
函数调用树是比函数调用链更为复杂的观察对象。如果能够显示调用树,就可以对调用的整个过程有个直观的了解。
调用树的分类
函数调用树有两类:
1. 抽象调用树
也叫虚拟调用树。比如在源码中,父函数调用了子函数a, b, c。那么对这三个函数的调用逻辑都考虑进去,这就是“抽象调用”。抽象调用树能全面的描述了父函数的逻辑和代码开发员的意图。但是,在实际的环境中,这三个函数未必就全部会调用到。把在实际的具体情况下未调用的“潜在”调用关系去掉后,剩下的调用树就称为“具体调用树”。明显,具体调用树不能全面显示代码开发员的意图,只是放映具体环境下函数的调用关系。
2. 具体调用树
也叫实时调用树。解释见上。
调用树的显示
1. 抽象调用树的显示
借助source insight等工具可以图形显示抽象调用树。
1. 具体调用树的显示
据本人的了解,目前gdb没有一个类似”bt”那样的能显示函数调用树的命令,但是借助gdb宏也许能够实现显示调用树的功能,这有待研究。不过,目前已经有个现成的调试工具可以显示调用树,它就是 systemtap.
效果如下:
[...]
0 klogd(1391):->sys_read
14 klogd(1391): ->fget_light
22 klogd(1391): <-fget_light
27 klogd(1391): ->vfs_read
35 klogd(1391): ->rw_verify_area
43 klogd(1391): <-rw_verify_area
49 klogd(1391): ->kmsg_read
0 sendmail(1696):->sys_read
17 sendmail(1696): ->fget_light
26 sendmail(1696): <-fget_light
34 sendmail(1696): ->vfs_read
44 sendmail(1696): ->rw_verify_area
52 sendmail(1696): <-rw_verify_area
58 sendmail(1696): ->proc_file_read
70 sendmail(1696): ->loadavg_read_proc
84 sendmail(1696): ->proc_calc_metrics
92 sendmail(1696): <-proc_calc_metrics
95 sendmail(1696): <-loadavg_read_proc
101 sendmail(1696): <-proc_file_read
106 sendmail(1696): ->dnotify_parent
115 sendmail(1696): <-dnotify_parent
119 sendmail(1696): ->inotify_dentry_parent_queue_event
127 sendmail(1696): <-inotify_dentry_parent_queue_event
133 sendmail(1696): ->inotify_inode_queue_event
141 sendmail(1696): <-inotify_inode_queue_event
146 sendmail(1696): <-vfs_read
151 sendmail(1696):<-sys_read
[...]
见于
http://sourceware.org/systemtap/wiki/WSCallGraph?highlight=1)
调用树的拼接
对于一个更刁的函数调用来说,利用工具显示的抽象调用树和具体调用调用树可能是不完整的。比如,对于抽象调用树来说,它的显示工具是source insight。但是如果这个函数对某个子函数或在更下层的函数对下下层的函数调用是通过函数指针来调用的,那么source insight显示的调用树中就会漏掉通过函数指针调用的子函数,以及以子函数为根的子调用树。这是因为函数指针变量的赋值是发生在代码动态运行时的。source insight无法利用静态的源码就捕捉到未来才出现东西,甚至它也无法在形式上解析出“那里存在一个利用函数指针的调用”。这就要通过阅读源码来找出这种调用关系。同时,可以利用调试器实时找出具体情况下是通过那个函数指针调用了哪个特定的下层函数。这样就能把漏掉的子调用树拼接到父调用树中。
可见,这些内容又回归到了调用链的内容。具体看前面。
调用图
各函数间的像蜘蛛网一样的调用关系的图形表示就是调用图了,显然它比调用树更复杂。
穿越盲区
本节意义:经过上面章节的叙述,利用源码交叉索引工具+调试器已经能解决大部分问题,但是因为调试器和交叉索引工具的各自局限性,依然会存在一些问题。本节尝试如何联合交叉索引工具以及调试器再加上人脑来解决各自的缺点。
[观察积累中,待扩展]
穿越gdb的盲区
进程切换
中断异常
系统调用
穿越交叉索引工具的盲区
函数指针
该小节内容移到了: 调用链的状态→函数指针调用
查看函数的参数
我们知道,一个函数的计算结果并不都是通过它的返回值返回的,有时会通过函数的参数返回真正感兴趣的数据。看内核源码的时候,如果调用链过长,涉及内容和数据结构过多的话,往往是看到最后都记不住函数的参数哪些是已经“初始化的”。
这也是交叉索引工具无法克服的先天弱点。它能动态索引源码,却无法动态查看数据。此时,可以利用gdb给目标函数下断点,而后可以用命令info args查看参数,另外命令info local可查看本地变量。当然在ddd下查看效果会更好。
内容简单,不展开了。
工程方法
二叉断点
实例 “什么/proc下无法创建目录?”
给调用指令下断点
如果对目标函数下断点后,受到很多骚扰,那么就转为在上层函数内对目标函数的调用指令处下断点。如果你已经进入了上层函数,对调用指令下断点,是更为精确的断点方法。
绕过时钟中断的干扰
有时我们调试的程序与中断无关的,但是由于时钟中断的异步到来,在调试过程中经常会自动进入时钟中断处理例程中,这严重干扰了我们的工作。用下面的方法可绕过时钟中断的干扰。
注:
使用GDB与QEMU调试内核时的问题分析: http://www.chinaitlab.com/linux/kernel/356774.html
关于qemu在单步指令时进入时钟中断的问题,上面给出链接给出了比较“深入”地探讨。这个问题涉及虚拟机本身,有人说是虚拟机相对于真机的固有缺陷,似乎很深奥,我没那个能力也没那个时间研究。但是我们应该知道,如果问题足够的复杂,以至于解决它要花费太高的代价,那么绕过这个问题是个更明智的解决方法。
解决方法(手工)
1. 内核启动早期
事先下两个断点
b common_interrupt
b native_iret
自定义返回命令
(gdb) define ooi
Type commands for definition of "ooi".
End with a line saying just "end".
>c
>stepi
>end
一旦时钟中断产生,就会拦截在中断处理的通用入口common_interrupt,然后运行返回指令,就会“回到”被时钟中断打断的原指令处
ooi
2. 内核启动完毕
事先下两个断点
b apic_timer_interrupt
b irq_return
一旦时钟中断产生,就会拦截在中断处理例程apic_timer_interrupt,然后运行返回指令,就会“回到”被时钟中断打断的原指令处
ooi
分析记录,待整理
提示,分析异常和中断的处理过程比分析C代码更直观,因为源码本身是汇编码。
┌──arch/x86/kernel/entry_32.S─────────────────────────────────────────────────────────────────────────────────────────────┐
│614 SAVE_ALL │
│615 TRACE_IRQS_OFF │
│616 movl %esp,%eax │
│617 call do_IRQ │
>│618 jmp ret_from_intr │
│619 ENDPROC(common_interrupt) │
│620 CFI_ENDPROC │
│621 │
│622 #define BUILD_INTERRUPT(name, nr) / │
│623 ENTRY(name) / │
│624 RING0_INT_FRAME; / │
│625 pushl $~(nr); / │
│626 CFI_ADJUST_CFA_OFFSET 4; / │
│627 SAVE_ALL; / │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│0xc01043e1 mov %edx,%ds │
│0xc01043e3 mov %edx,%es │
│0xc01043e5 mov $0xd8,%edx │
│0xc01043ea mov %edx,%fs │
│0xc01043ec mov %esp,%eax │
│0xc01043ee call 0xc0106151 │
>│0xc01043f3 jmp 0xc01038dc │
│0xc01043f8 push $0xffffff03 │
│0xc01043fd cld │
│0xc01043fe push %fs │
│0xc0104400 push %es │
│0xc0104401 push %ds │
│0xc0104402 push %eax │
│0xc0104403 push %ebp │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: common_interrupt Line: 618 PC: 0xc01043f3
(gdb)
(gdb)
(gdb) bt
#0 common_interrupt () at arch/x86/kernel/entry_32.S:618
#1 0x00000292 in ?? ()
#2 0xc01880db in alloc_vfsmnt (name=0xc031dcf3 "rootfs") at include/linux/slab.h:266
#3 0xc0176919 in vfs_kern_mount (type=0xc0359678, flags=0, name=0xc031dcf3 "rootfs", data=0x0) at fs/super.c:896
#4 0xc0176a2f in do_kern_mount (fstype=0xc031dcf3 "rootfs", flags=0, name=0xc031dcf3 "rootfs", data=0x0) at fs/super.c:968
#5 0xc0393b33 in mnt_init () at fs/namespace.c:2285
#6 0xc039382b in vfs_caches_init (mempages=108676) at fs/dcache.c:2212
#7 0xc037f868 in start_kernel () at init/main.c:666
#8 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#9 0x00000000 in ?? ()
(gdb) disass
(gdb)
----
┌──arch/x86/kernel/entry_32.S─────────────────────────────────────────────────────────────────────────────────────────────┐
│401 cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax │
│402 CFI_REMEMBER_STATE │
│403 je ldt_ss # returning to user-space with LDT SS │
│404 restore_nocheck: │
│405 TRACE_IRQS_IRET │
│406 restore_nocheck_notrace: │
│407 RESTORE_REGS │
│408 addl $4, %esp # skip orig_eax/error_code │
│409 CFI_ADJUST_CFA_OFFSET -4 │
│410 irq_return: │
>│411 INTERRUPT_RETURN │
│412 .section .fixup,"ax" │
│413 ENTRY(iret_exc) │
│414 pushl $0 # no error code │
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│0xc0103a61 pop %ebx │
│0xc0103a62 pop %ecx │
│0xc0103a63 pop %edx │
│0xc0103a64 pop %esi │
│0xc0103a65 pop %edi │
│0xc0103a66 pop %ebp │
│0xc0103a67 pop %eax │
│0xc0103a68 pop %ds │
│0xc0103a69 pop %es │
│0xc0103a6a pop %fs │
│0xc0103a6c add $0x4,%esp │
>│0xc0103a6f jmp *%cs:0xc0353b54 │
│0xc0103a76 lar 0x3c(%esp),%eax │
│0xc0103a7b jne 0xc0103a61 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: irq_return Line: 411 PC: 0xc0103a6f
(gdb) stepi
0xc0103a64 in restore_nocheck_notrace () at arch/x86/kernel/entry_32.S:407
0xc0103a65 in restore_nocheck_notrace () at arch/x86/kernel/entry_32.S:407
0xc0103a66 in restore_nocheck_notrace () at arch/x86/kernel/entry_32.S:407
Watchpoint 3: $ebp
Old value = (void *) 0xc0378000
New value = (void *) 0xc0379f4c
0xc0103a67 in restore_nocheck_notrace () at arch/x86/kernel/entry_32.S:407
0xc0103a68 in restore_nocheck_notrace () at arch/x86/kernel/entry_32.S:407
0xc0103a69 in restore_nocheck_notrace () at arch/x86/kernel/entry_32.S:407
0xc0103a6a in restore_nocheck_notrace () at arch/x86/kernel/entry_32.S:407
restore_nocheck_notrace () at arch/x86/kernel/entry_32.S:408
irq_return () at arch/x86/kernel/entry_32.S:411
(gdb)
------------
┌──arch/x86/kernel/entry_32.S─────────────────────────────────────────────────────────────────────────────────────────────┐
│864 jmp irq_return │
│865 CFI_ENDPROC │
│866 KPROBE_END(nmi) │
│867 │
│868 #ifdef CONFIG_PARAVIRT │
│869 ENTRY(native_iret) │
>│870 iret │
│871 .section __ex_table,"a" │
│872 .align 4 │
│873 .long native_iret, iret_exc │
│874 .previous │
│875 END(native_iret) │
│876 │
│877 ENTRY(native_irq_enable_syscall_ret) │
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
>│0xc01045a8 iret │
│0xc01045a9 lea 0x0(%esi),%esi │
│0xc01045ac sti │
│0xc01045ad sysexit │
│0xc01045af nop │
│0xc01045b0 push $0x0 │
│0xc01045b2 push $0xc0105030 │
│0xc01045b7 jmp 0xc02bfb80 │
│0xc01045bc push $0x0 │
│0xc01045be push $0xc0104fe3 │
│0xc01045c3 jmp 0xc02bfb80 │
│0xc01045c8 push $0x0 │
│0xc01045ca push $0xc0104f6d │
│0xc01045cf jmp 0xc02bfb80 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: native_iret Line: 870 PC: 0xc01045a8
#9 0x00000000 in ?? ()
(gdb) stepi
native_iret () at arch/x86/kernel/entry_32.S:870
(gdb) bt
#0 native_iret () at arch/x86/kernel/entry_32.S:870
#1 0xc0172198 in kmem_cache_alloc (cachep=0xdc404ec0, flags=) at include/asm/string_32.h:183
#2 0xc01880db in alloc_vfsmnt (name=0xc031dcf3 "rootfs") at include/linux/slab.h:266
#3 0xc0176919 in vfs_kern_mount (type=0xc0359678, flags=0, name=0xc031dcf3 "rootfs", data=0x0) at fs/super.c:896
#4 0xc0176a2f in do_kern_mount (fstype=0xc031dcf3 "rootfs", flags=0, name=0xc031dcf3 "rootfs", data=0x0) at fs/super.c:968
#5 0xc0393b33 in mnt_init () at fs/namespace.c:2285
#6 0xc039382b in vfs_caches_init (mempages=108676) at fs/dcache.c:2212
#7 0xc037f868 in start_kernel () at init/main.c:666
#8 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#9 0x00000000 in ?? ()
(gdb)
-----------------------------
┌──mm/slab.c──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│3478 if (unlikely((flags & __GFP_ZERO) && objp)) │
│3479 memset(objp, 0, obj_size(cachep)); │
│3480 │
│3481 return objp; │
│3482 } │
│3483 │
│3484 /* │
│3485 * Caller needs to acquire correct kmem_list's list_lock │
│3486 */ │
│3487 static void free_block(struct kmem_cache *cachep, void **objpp, int nr_objects, │
│3488 int node) │
│3489 { │
│3490 int i; │
│3491 struct kmem_list3 *l3; │
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│0xc0172183 cmpw $0x0,-0x14(%ebp) │
│0xc0172188 jns 0xc01721a7 │
│0xc017218a test %edi,%edi │
│0xc017218c je 0xc01721a7 │
│0xc017218e mov 0x2c(%esi),%edx │
│0xc0172191 xor %eax,%eax │
│0xc0172193 mov %edx,%ecx │
│0xc0172195 shr $0x2,%ecx │
>│0xc0172198 rep stos %eax,%es:(%edi) │
│0xc017219a test $0x2,%dl │
│0xc017219d je 0xc01721a1 │
│0xc017219f stos %ax,%es:(%edi) │
│0xc01721a1 test $0x1,%dl │
│0xc01721a4 je 0xc01721a7 │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: kmem_cache_alloc Line: 183 PC: 0xc0172198
#7 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#8 0x00000000 in ?? ()
(gdb) bt
#0 0xc0172198 in kmem_cache_alloc (cachep=0xdc404ec0, flags=) at include/asm/string_32.h:183
#1 0xc01880db in alloc_vfsmnt (name=0xc031dcf3 "rootfs") at include/linux/slab.h:266
#2 0xc0176919 in vfs_kern_mount (type=0xc0359678, flags=0, name=0xc031dcf3 "rootfs", data=0x0) at fs/super.c:896
#3 0xc0176a2f in do_kern_mount (fstype=0xc031dcf3 "rootfs", flags=0, name=0xc031dcf3 "rootfs", data=0x0) at fs/super.c:968
#4 0xc0393b33 in mnt_init () at fs/namespace.c:2285
#5 0xc039382b in vfs_caches_init (mempages=108676) at fs/dcache.c:2212
#6 0xc037f868 in start_kernel () at init/main.c:666
#7 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#8 0x00000000 in ?? ()
(gdb) list
(gdb) disass
(gdb)
bug 与 OOPS
[主要研究定位bug的技巧,找出是哪条指令引发了panic似乎很容易。但要找出错误产生的源头似乎是门艺术了]
经过上面章节的叙述,本小节问题的解决已不成问题了。不再展开叙述。可以参考下面链接。
参考手册
“Using kgdb and the kgdb Internals” http://www.kernel.org/pub/linux/kernel/people/jwessel/kgdb/index.html
kgdb官网 http://kgdb.linsyssoft.com/
参考书籍(freeebsd)
“Debugging Kernel Problems” http://www.google.cn/search?q=Debugging+Kernel+Problems&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:zh-CN:unofficial&client=firefox-a
“Chapter 10 Kernel Debugging” http://www.freebsd.org/doc/en_US.ISO8859-1/books/developers-handbook/kerneldebug.html
参考书籍(linux)
Chapter 14. Kernel Debugging Techniques of “Embedded Linux Primer: A Practical, Real-World Approach”
http://book.opensourceproject.org.cn/embedded/embeddedprime/
参考文章
“掌握 Linux 调试技术” http://www.ibm.com/developerworks/cn/linux/sdk/l-debug/index.html
“定位Oops的具体代码行” http://blog.chinaunix.net/u/12592/showart_1092733.html
“跟踪内核 oops” http://wiki.zh-kernel.org/doc/oops-tracing.txt
“例解Linux Kernel Debug” http://blog.chinaunix.net/u/2108/showart_164703.html
“kernel debug的一些小手段” http://blog.chinaunix.net/u/12592/showart_499502.html
“Kernel Debugging Techniques” http://www.linuxjournal.com/article/9252
[参考文章]有的已过时,而且深度不够。
网站
http://bugzilla.kernel.org/
http://www.kerneloops.org/
http://www.lkml.org/ 搜索bug
***第二部分:内核分析***
这部分的内容侧重于内核原理分析,其中涉及gdb调试器的内容不是很多,但它起的作用很关键,主要用于观察内核数据的生成及变化,在对源码理解有困惑时用于验证自己的猜想。另外,gdb一个很重要的功能是,拦截通过函数指针调用的函数,从而追溯整个调用链,交叉索引工具无法做到这点。
另外,调试内核时,利用gdb的“list 函数名”命令看到的C代码都是当前处理器当前配置下内核实际运行的函数版本:”disass 函数名”看到的都是处理器实际运行时的机器代码,也就是说define语句和inline函数已经被编译器处理了,而且编译器也完成了优化。所以,gdb本身就是一种不可替代的源码浏览工具,它能筛选掉出实际运行的函数版本,又能呈现出实际运行的机器码。
调试相关子系统
kgdb源码分析
gdb远程串口协议
http://sourceware.org/gdb/current/onlinedocs/gdb_34.html#SEC706
http://www.huihoo.org/mirrors/pub/embed/document/debugger/ew_GDB_RSP.pdf
Jason Wessel的linux-2.6-kgdb.git
http://git.kernel.org/?p=linux/kernel/git/jwessel/linux-2.6-kgdb.git;a=summary
gdb调试模式
(gdb) set debug serial 1
(gdb) set debug remote 1
sysrq
oprofile
kprobes
驱动分析
[分析一个简单的驱动,观察函数调用流程。重点观察驱动与驱动模型,以及和系统内核的交互过程。比如,中断的整个生命周期。]
参考:
“Debugging kernel modules” http://lwn.net/Articles/90913/
“Linux 系统内核的调试” http://www.ibm.com/developerworks/cn/linux/l-kdb/
“Linux 可加载内核模块剖析” http://www.ibm.com/developerworks/cn/linux/l-lkm/
“使用 KGDB 调试 Linux 内核” http://blog.chinaunix.net/u/8057/showart_1087126.html
“使用 /proc 文件系统来访问 Linux 内核的内容” http://www.ibm.com/developerworks/cn/linux/l-proc.html
如何查找出当前系统所安装模块驱动对应的源码,从而对其做些修改等实验?
提示:
1. lsmod 列出模块名
2. modinfo 模块名, 查看模块信息
3. 模块名,模块信息中的别名,模块的参数说明文字都可结合source insight查找该模块的源码文件;模块信息中的模块路径也可用来定位对应源码的路径以及相关的kconfig文件,从而获取更多相关信息。一般源码文件的名称就是模块名或在模块名的基础上加上某些后缀,用模块名的方法查找不出时再利用其他信息查找。
4. 如果利用以上方法还找不到源文件,或者一个模块对应着几个源文件,可使用最后的必杀绝招。比如lsmod后得到一个sr_mod。我们用modinfo sr_mod的得到它的已编译文件的路径是 /lib/modules/2.6.24-19-generic/kernel/drivers/scsi/sr_mod.ko ;把它拷贝出来,并用命令objdump -d sr_mod.ko 查看它的机器码,就可以知道它使用了哪些函数,利用这些函数名就可以结合source insight搜索出源码了。
载入模块符号
首先,在虚拟系统上装入目标模块foo,然后到/sys/module/foo/sections/下查看目标模块的section偏移地址信息.
实例
debian:/sys/module/smplefs/sections# cat .text .data .bss
0xe01c7000
0xe01c864c
0xe01c8b20
然后,到真机的gdb下用add-symbol-file命令装载目标模块的符号信息 格式如下
add-symbol-file /path/to/module 0xe01c7000 / # .text
-s .data 0xe01c864c /
-s .bss 0xe01c8b20
实例
(gdb) add-symbol-file test/day11/samplefs.ko 0xe01c7000 -s .data 0xe01c864c -s .bss 0xe01c8b20
add symbol table from file "test/day11/samplefs.ko" at
.text_addr = 0xe01c7000
.data_addr = 0xe01c864c
.bss_addr = 0xe01c8b20
(y or n) y
Reading symbols from /storage/myqemu/new/linux-2.6.26/test/day11/samplefs.ko...done.
(gdb)
然后,余下的对模块的调试就类似对内核的调试了。
seq_file.c的分析
module.c的分析
中断处理过程
s3c24xx内存初始化分析
[从这节开始,侧重于利用kgdb和source insight理解内核原理] [网上好像没这个内容。只看源码的话,因为source insight不能解析汇编源文件,在汇编源码中定位到初始化的源头好像很难,利用调试器很容易做到这点]
虚拟地址空间
用户层的观察窗
[待充实]
3G~4G虚拟地址空间的用途。(来自于qemu虚拟机的dmesg启动信息,500m物理内存)
<4>Zone PFN ranges:
<4> DMA 0 -> 4096
<4> Normal 4096 -> 127984
<4> HighMem 127984 -> 127984
<6>virtual kernel memory layout:
<4> fixmap : 0xfff4c000 - 0xfffff000 ( 716 kB)
<4> pkmap : 0xff800000 - 0xffc00000 (4096 kB)
<4> vmalloc : 0xe0000000 - 0xff7fe000 ( 503 MB)
<4> lowmem : 0xc0000000 - 0xdf3f0000 ( 499 MB)
<4> .init : 0xc037f000 - 0xc03bb000 ( 240 kB)
<4> .data : 0xc02c0875 - 0xc03773ac ( 730 kB)
<4> .text : 0xc0100000 - 0xc02c0875 (1794 kB)
3G~4G虚拟地址空间的用途。(来自于qemu虚拟机的dmesg启动信息,897m物理内存)
<4>Zone PFN ranges:
<4> DMA 0 -> 4096
<4> Normal 4096 -> 229376
<4> HighMem 229376 -> 229616
<6>virtual kernel memory layout:
<4> fixmap : 0xfff4c000 - 0xfffff000 ( 716 kB)
<4> pkmap : 0xff800000 - 0xffc00000 (4096 kB)
<4> vmalloc : 0xf8800000 - 0xff7fe000 ( 111 MB)
<4> lowmem : 0xc0000000 - 0xf8000000 ( 896 MB)
<4> .init : 0xc037f000 - 0xc03bb000 ( 240 kB)
<4> .data : 0xc02c0875 - 0xc03773ac ( 730 kB)
<4> .text : 0xc0100000 - 0xc02c0875 (1794 kB)
3G~4G虚拟地址空间的用途。(来自真机的dmesg启动信息,3G物理内存)
[ 0.000000] Zone PFN ranges:
[ 0.000000] DMA 0 -> 4096
[ 0.000000] Normal 4096 -> 229376
[ 0.000000] HighMem 229376 -> 786416
[ 33.262853] virtual kernel memory layout:
[ 33.262854] fixmap : 0xfff4b000 - 0xfffff000 ( 720 kB)
[ 33.262855] pkmap : 0xff800000 - 0xffc00000 (4096 kB)
[ 33.262856] vmalloc : 0xf8800000 - 0xff7fe000 ( 111 MB)
[ 33.262857] lowmem : 0xc0000000 - 0xf8000000 ( 896 MB)
[ 33.262858] .init : 0xc0421000 - 0xc047d000 ( 368 kB)
[ 33.262859] .data : 0xc03204c4 - 0xc041bdc4 (1006 kB)
[ 33.262861] .text : 0xc0100000 - 0xc03204c4 (2177 kB)
top, 4G --->+-------------------+
| |
| malloc()'ed memory|
| interrupt stack | kernel
| data |
| text |
kernel, 3G--->+-------------------+
| |
| argv,envp |
| user stack |
| | |
| | |
| v |
| | user process
| ^ |
| | |
| | |
| heap |
| data |
| text |
user, 0G---> +-------------------+
Layout of virtual address space
我们验证一下用户空间的内容(上图的下部分)[未完,待续] 引用于http://linux.chinaunix.net/bbs/viewthread.php?tid=978491
查看进程的虚拟地址空间是如何使用的。
该文件有6列,分别为:
地址:库在进程里地址范围
权限:虚拟内存的权限,r=读,w=写,x=,s=共享,p=私有;
偏移量:库在进程里地址范围
设备:映像文件的主设备号和次设备号;
节点:映像文件的节点号;
路径: 映像文件的路径
每项都与一个vm_area_struct结构成员对应,
----
struct vm_area_struct {
struct mm_struct * vm_mm; /* The address space we belong to. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, listed below. */
struct rb_node vm_rb;
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap prio tree, or
* linkage to the list of like vmas hanging off its node, or
* linkage of vma in the address_space->i_mmap_nonlinear list.
*/
union {
struct {
struct list_head list;
void *parent; /* aligns with prio_tree_node parent */
struct vm_area_struct *head;
} vm_set;
struct raw_prio_tree_node prio_tree_node;
} shared;
/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_node; /* Serialized by anon_vma->lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
struct vm_operations_struct * vm_ops;
/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
unsigned long vm_truncate_count;/* truncate_count or restart_addr */
#ifndef CONFIG_MMU
atomic_t vm_usage; /* refcount (VMAs shared if !MMU) */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
[todo 换个简单的程序]
$ ps -aux | grep firefox
Warning: bad ps syntax, perhaps a bogus '-'? See http://procps.sf.net/faq.html
fqh 8230 4.7 2.5 205872 80024 ? Tl 14:54 0:19 /usr/lib/firefox-3.0.1/firefox
fqh 8313 0.0 0.0 3220 764 pts/1 R+ 15:01 0:00 grep firefox
(gdb) attach 8230
...
.....
Loaded symbols for /usr/lib/libflashsupport.so
Reading symbols from /usr/lib/libpulse.so.0...(no debugging symbols found)...done.
Loaded symbols for /usr/lib/libpulse.so.0
Reading symbols from /lib/libcap.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/libcap.so.1
(no debugging symbols found)
0xb7f24410 in __kernel_vsyscall ()
(gdb) bt
#0 0xb7f24410 in __kernel_vsyscall ()
#1 0xb7d46c07 in poll () from /lib/tls/i686/cmov/libc.so.6
#2 0xb6b4e1c6 in ?? () from /usr/lib/libglib-2.0.so.0
#3 0xb6b4e74e in g_main_context_iteration () from /usr/lib/libglib-2.0.so.0
#4 0xb77ba87c in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#5 0xb77cf624 in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#6 0xb77cfa6f in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#7 0xb787ecd6 in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#8 0xb784e31f in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#9 0xb77cf75e in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#10 0xb765f122 in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#11 0xb70b3a88 in XRE_main () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#12 0x08049033 in ?? ()
#13 0xb7c90450 in __libc_start_main () from /lib/tls/i686/cmov/libc.so.6
#14 0x08048cc1 in ?? ()
(gdb)
$ cat /proc/8230/maps
08048000-0804f000 r-xp 00000000 08:01 7022914 /usr/lib/firefox-3.0.1/firefox <-text,注意标志 可读可执行不可写私有
0804f000-08050000 rw-p 00006000 08:01 7022914 /usr/lib/firefox-3.0.1/firefox <-data,注意标志 可读可写不可执行
08050000-0abd4000 rw-p 08050000 00:00 0 [heap] <-heap,一共45.5多MB[todo:验证向上增长]可读可写不可执行
ae060000-ae063000 r-xp 00000000 08:01 6941098 /usr/lib/libflashsupport.so <-libflashsupport.so 共享库的代码段, 可读可执行不可写
ae063000-ae064000 rw-p 00002000 08:01 6941098 /usr/lib/libflashsupport.so <-libflashsupport.so 共享库的数据段, 可读可写不可执行
.....
..
b7f20000-b7f21000 rw-p 00001000 08:01 6942869 /usr/lib/libplds4.so.0d
b7f21000-b7f22000 r--p 00000000 08:01 6966184 /usr/lib/locale/zh_CN.utf8/LC_IDENTIFICATION
b7f22000-b7f24000 rw-p b7f22000 00:00 0
b7f24000-b7f25000 r-xp b7f24000 00:00 0 [vdso]
b7f25000-b7f3f000 r-xp 00000000 08:01 2326545 /lib/ld-2.7.so
b7f3f000-b7f41000 rw-p 00019000 08:01 2326545 /lib/ld-2.7.so
bfbcd000-bfc0a000 rw-p bffc3000 00:00 0 [stack] <-stack,不到0.24MB,可读可执行不可写[todo:验证向下增长]
[todo:验证argv,envp]
$
交互,从内核层分析
[扩展]
理解设备模型
[结合source insight分析一个内核子系统的原理。源码分析工具虽好,但却是个死的东西,不能实时观察数据的生成和变化。如果在内核运行的时候,搭配调试器来分析,这个过程一定很形象和有趣]
面向对象的实现
设备模型的分层
外围支持机制
sysfs
hotplug
文件系统
参考书籍:
UNIX Filesystems Evolution, Design, and Implementation.pdf :
http://www.google.cn/search?q=UNIX+Filesystems+Evolution%2C+Design%2C+and+Implementation&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:zh-CN:unofficial&client=firefox-a
站点:
Ext4 (and Ext2/Ext3) Wiki: http://ext4.wiki.kernel.org/index.php/Main_Page
Ext4 Development project: http://www.bullopensource.org/ext4/
ext2-devel maillist archive: http://sourceforge.net/mailarchive/forum.php?forum=ext2-devel
参考文章:
“Linux Filesystems in 21 days 45 minutes” http://us1.samba.org/samba/ftp/cifs-cvs/ols2006-fs-tutorial-smf.pdf
***第三部分:其他工具***
strace
作用: strace能拦截和记录应用程序发起的系统调用和它收到的信号。主要用于观察应用层和内核层的交互。
命令选项: 查看,$strace –help 或$man strace 或 $info strace
实例
ltrace
作用: ltrace用于监控程序发起的库函数调用以及程序收到的信号。
SystemTap
动态收集Linux内核信息和性能数据
官方 http://sourceware.org/systemtap/
参考文章
http://www.ibm.com/developerworks/cn/linux/l-cn-systemtap3/index.html
http://www.ibm.com/developerworks/cn/linux/l-systemtap/index.html
http://sourceware.org/systemtap/tutorial/
http://sourceware.org/systemtap/wiki
ubuntu下的配置安装: http://sourceware.org/systemtap/wiki/SystemtapOnUbuntu
MEMWATCH
作用: 跟踪程序中的内存泄漏和错误
YAMD
作用: 查找 C 和 C++ 中动态的、与内存分配有关的问题
Magic SysRq
内核文档 sysrq.txt
http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=blob;f=Documentation/sysrq.txt;h=10a0263ebb3f01e832c7827cc75d7fe54b341a6f;hb=f8d56f1771e4867acc461146764b4feeb5245669
linux内核测试指南 相关章节
http://wiki.zh-kernel.org/#%E6%96%87%E7%AB%A0
附录:社区交流相关
补丁提交相关文档
如何参与 Linux 内核开发
http://wiki.zh-kernel.org/doc/howto
Linux内核代码风格
http://wiki.zh-kernel.org/doc/codingstyle
Linux内核开发邮件客户端资料
http://wiki.zh-kernel.org/doc/email-clients.txt
Linux内核补丁提交注意事项
http://wiki.zh-kernel.org/doc/linux%E5%86%85%E6%A0%B8%E8%A1%A5%E4%B8%81%E6%8F%90%E4%BA%A4%E6%B3%A8%E6%84%8F%E4%BA%8B%E9%A1%B9
基于git的Gentoo中文文档开发流程
http://www.gentoo-cn.org/doc/zh_cn/git-howto.xml
mutt配置使用
http://hi.baidu.com/springtty/blog/item/e6b25ddbb52f51ddb7fd4805.html
http://www.kongove.cn/blog/?p=149
http://www.kongove.cn/blog/?p=229
http://www.kongove.cn/blog/?p=225
补丁制作与提交示范
说明:这个补丁本是用web-gmail发的。后来发现web-gmail遇到一行字数很长的补丁时,会导致补丁格式错误。本人决定以后使用claws,看的是它有线索成组的功能和草稿上有字数的标尺。内核社区中使用的邮件客户端大多数是mutt(并不是专指发补丁)。发补丁的所用工具也多种多样,有用各种邮件客户端的(mutt,claws,kmail...),有使用git-send-email的,还有使用quilt的,真是打开眼界。
另外本人感觉claws比Sylpheed要快上几十倍。claws本来是Sylpheed的实验版,后来独立出来了。
补丁的任务
mm/oom_kill.c:badness()函数
/**
* badness - calculate a numeric value for how bad this task has been
* @p: task struct of which task we should calculate
* @uptime: current uptime in seconds
* @mem: target memory controller//<-Li Zefan大侠上次提交了一个补丁,去掉了badness()的这个参数,
但是忘了删除该参数的说明了。现在的任务是提交补丁把它删除掉
*
...省略
*/
unsigned long badness(struct task_struct *p, unsigned long uptime)
{
过程
1. 进入git树
XXX@ubuntu:~$ cd /storage/linus-git/linux-2.6/
XXX@ubuntu:/storage/linus-git/linux-2.6$
2. 更新git树
XXX@ubuntu:/storage/linus-git/linux-2.6$ git-pull
Already up-to-date.
3. 修改目标源码
XXX@ubuntu:/storage/linus-git/linux-2.6$ vi mm/oom_kill.c
删除掉那个参数说明后结束vi,返回到shell下
4. 制作补丁
XXX@ubuntu:/storage/linus-git/linux-2.6$ git-diff > ../oom_kill.patch
5. 还原git树
XXX@ubuntu:/storage/linus-git/linux-2.6$ patch -p1 < ../oom_kill.patch -R
patching file mm/oom_kill.c
还原的方法或者采用下面方式
XXX@ubuntu:/storage/linus-git/linux-2.6$ git-gui
界面出来后,点击Branch->reset
6.点击compose新建邮件
7. 点击insert file 载入补丁文件
diff --git a/mm/oom_kill.c b/mm/oom_kill.c
index 64e5b4b..460f90e 100644
--- a/mm/oom_kill.c
+++ b/mm/oom_kill.c
@@ -38,7 +38,6 @@ static DEFINE_SPINLOCK(zone_scan_mutex);
* badness - calculate a numeric value for how bad this task has been
* @p: task struct of which task we should calculate
* @uptime: current uptime in seconds
- * @mem: target memory controller
*
* The formula used is relatively simple and documented inline in the
* function. The main rationale is that we want to select a good task
8. 补全其他信息,比如 标题,to, cc等,还有信件内容
本例是:
标题起为:[PATCH]mm/oom_kill.c: cleanup kerneldoc of badness()
//为讨好Randy.Dunlap,特意写了字眼kerneldoc,因为他是主管内核文档的
to "Randy.Dunlap"
//收件人是谁得根据补丁的性质查看内核源码中的MAINTAINERS文件,
//难以确认是谁时,可以到linus-git的web-git下参看你修改文件的历史记录,看别人是发给谁的
cc [email protected], //这个一定要有
[email protected] //修改文件所在的子系统的邮件列表,当收件人写错时,
//子系统的头目们可能会注意到和接受你的补丁
邮件本身处理后变成下面的格式
Paramter @mem has been removed since v2.6.26, now delete it's comment. //补丁的作用
Signed-off-by: your-name //你的签收
--- //三个'-',表示下面的内容是补丁了。应用补丁的工具会根据这个标志提取补丁。
diff --git a/mm/oom_kill.c b/mm/oom_kill.c
index 64e5b4b..460f90e 100644
--- a/mm/oom_kill.c
+++ b/mm/oom_kill.c
@@ -38,7 +38,6 @@ static DEFINE_SPINLOCK(zone_scan_mutex);
* badness - calculate a numeric value for how bad this task has been
* @p: task struct of which task we should calculate
* @uptime: current uptime in seconds
- * @mem: target memory controller
*
* The formula used is relatively simple and documented inline in the
* function. The main rationale is that we want to select a good task
9. 然后发信
10. 等待回复
Randy Dunla果然勤快,两个小时不到就收到了他的信件
Acked-by: Randy Dunlap
Thanks.
说明:
Acked-by表示他认为补丁正确,但并不自己接收。 维护者回复applied才算是接受了。
当然如果得到比较有威望的人acked-by,被接受的可能性就大大的提高了。--Li Yang大牛
看来,我把补丁的维护人搞错了,因为Randy Dunlap并没领我的情:(
注意,这个不能急。有的补丁或许是因为太微小,对方都没回复你,其实他已经收录了你的补丁。
到子系统树向linus的git树合并时就会看到你的补丁(通常会在LKML中有个集体通告,说明
这批补丁中包含了哪些内容。)。等一个多星期无妨。
后记:过了三四天后,收到了一封信如下。这是mm树的邮件系统发来的。可见Randy Dunlap
把这个补丁提交给mm树了。在mm树经过验证补丁正确后就会再汇合到linus的主线树中。这是
补丁接受的一种方式。当然,我遇到的情况有,子系统维护人回复你applied to xx(树),
然后该负责人就要求linus merge他的树,这样就收不到mm树的通知信。甚至有时子系统维护
人接受补丁了都不吭一声,然后补丁又是直接merge到主线树中。补丁接受的流程大概就这样了。
From: [email protected]
To: [email protected]
Cc: [email protected], [email protected]
Subject: + mm-oom_killc-fix-badness-kerneldoc.patch added to -mm tree
Date: Thu, 30 Oct 2008 14:47:53 -0700
The patch titled
mm/oom_kill.c: fix badness() kerneldoc
has been added to the -mm tree. Its filename is
mm-oom_killc-fix-badness-kerneldoc.patch
....省略
The current -mm tree may be found at http://userweb.kernel.org/~akpm/mmotm/
------------------------------------------------------
Subject: mm/oom_kill.c: fix badness() kerneldoc
From: Qinghuang Feng
Paramter @mem has been removed since v2.6.26, now delete it's comment.
Signed-off-by: Qinghuang Feng
Acked-by: Randy Dunlap
Signed-off-by: Andrew Morton
---
...补丁内容省略
_
Patches currently in -mm which might be from [email protected] are
origin.patch
mm-oom_killc-fix-badness-kerneldoc.patch
linux-next.patch
git使用
Git 中文教程
http://www.linuxsir.org/main/doc/git/gittutorcn.htm
git使用小结
http://wangcong.org/blog/?p=307
学习 Git
http://www.zeuux.org/science/learning-git.cn.html
附录:内核参考书籍文章
内核git库:
http://git.kernel.org/?p=linux/kernel/git
综合类:
“understanding the linux kernel”
”linux kernel development“
“linux源代码情景分析”
“Embedded.Linux.Primer.A.Practical.Real.World.Approach.”
“The_Linux_Kernel_Primer_A_Top_Down_Approach_For_x86_and_PowerPC_Architectures”
子系统类:
文件系统:
“UNIX Filesystems Evolution, Design, and Implementation”
“File System Forensic Analysis”
“Windows NT File System Internals”
内存管理:
“Understanding The Linux Virtual Memory Manager”
网络系统:
“The Linux® Networking Architecture: Design and Implementation of Network Protocols in the Linux Kernel”
“Understanding.Linux.Network.Internals”
驱动开发:
“linux device drivers”
“Essential.Linux.Device.Drivers”
源码本身及附带文档
参考文章:
IBM-Linux 相关专题 http://www.ibm.com/developerworks/cn/linux/ “Debugging Kernel Modules with User Mode Linux”
http://www.linuxjournal.com/article/5749
“Debugging Memory on Linux” http://www.linuxjournal.com/article/4681
“DDD—Data Display Debugger” http://www.linuxjournal.com/article/2315
“Linux 系统内核的调试” http://www.ibm.com/developerworks/cn/linux/l-kdb/
System Dump和Core Dump的区别 http://hi.baidu.com/iruler/blog/item/c203de3522ff398ea61e122c.html
http://www.linuxjournal.com/user/800887/track
http://www.linuxjournal.com/ http://www.ibm.com/developerworks/cn/linux/l-devmapper/index.html
read 系统调用剖析 http://www.ibm.com/developerworks/cn/linux/l-cn-read/index.html
http://blog.chinaunix.net/u/4206/showart_501237.html
http://hi.baidu.com/linux%5Fkernel/blog/category/pci%C9%E8%B1%B8%C7%FD%B6%AF
http://wiki.jk2410.org/wiki/Main_Page
http://www.ibm.com/developerworks/cn/linux/l-cn-clocks/index.html
利用Vmware5.5.1 和 kgdb调试 x86平台的kernel
http://blog.chinaunix.net/u/22617/showart_338509.html
Welcome to Linux From Scratch
http://www.linuxfromscratch.org/
Unreliable Guide To Locking
http://www.kernel.org/pub/linux/kernel/people/rusty/kernel-locking/index.html
How do I printk correctly?
http://lkml.org/lkml/2008/10/23/132
KernelJanitors/Todo
http://kernelnewbies.org/KernelJanitors/Todo
Coccinelle - a Framework for Linux Device Driver Evolution
http://www.emn.fr/x-info/coccinelle/
linux论文 http://www.linuxsymposium.org
www.linuxsymposium.org/2006/linuxsymposium_procv2.pdf
www.linuxsymposium.org/2006/linuxsymposium_procv1.pdf
understanding the linux kernel 在线文档
http://www.linux-security.cn/ebooks/ulk3-html/
Data Structures and Algorithms with Object-Oriented Design Patterns in C++/Java/C#/Python/Ruby/Lua/Perl/PHP
http://www.brpreiss.com/books/opus4/
ftp://ftp.akaedu.org/../1.html
私人备忘
cpan设置
Going to read /home/fqh/.cpan/sources/modules/02packages.details.txt.gz
Warning: Your /home/fqh/.cpan/sources/modules/02packages.details.txt.gz does not contain a Line-Count header.
是选取站点不可用造成的。
http://tech.foolpig.com/2008/10/22/cpan-error-modulelist/
1.删除掉.cpan
2.perl -MCPAN -e shell
或1,2步骤换为o conf init命令
3.选了africa下的三个站点
4.cpan设置完后,reload index即可
5.列举模块m
6.查询 d /模块/
---
__attribute__((context(x,0,1))) means "you need not hold x before, but
you will hold one more of x after".
__attribute__((context(x,1,0))) means "you must already hold x, and you
will no longer hold x after".
__attribute__((context(x,1,1))) means "you must already hold x, and you
will continue to hold x".
1) WarStories