@xiaoxiang
2015-04-09T23:35:07.000000Z
字数 4203
阅读 6566
Linux内核分析
by 赵缙翔
原创作品转载请注明出处
这是我第一次写的博客,如有疏漏,还请指教。
在上完孟宁老师的软件工程课程后,觉得这老师的课真心不错,就又选了他的Linux内核分析。因为Linux内核代码中还是有一些C语言没法做的事情需要At&T汇编代码来帮忙,所以我们需要了解一些汇编的常识。
最先开始,Intel 8086和8088有十四个16位寄存器,比如AX
, BX
, CX
, DX
等等。然后Intel出了32位处理器,相对于16位处理器是是扩展的(extended),于是在16位的寄存器基础上加上E
前缀,比如AX
变成了EAX
,在后来,AMD出了64位处理器,采用的R
前缀,具体为什么用R
,我也不造啊,求告诉。
(有汇编基础的应该很好懂……我学过单片机的汇编,竟然也看懂了大部分。so,我就不赘述了,摘抄自wiki百科)
Although the main registers (with the exception of the instruction pointer) are "general-purpose" in the 32-bit and 64-bit versions of the instruction set and can be used for anything, it was originally envisioned that they be used for the following purposes:
- AL/AH/AX/EAX/RAX: Accumulator
- BL/BH/BX/EBX/RBX: Base index (for use with arrays)
- CL/CH/CX/ECX/RCX: Counter (for use with loops and strings)
- DL/DH/DX/EDX/RDX: Extend the precision of the accumulator (e.g. combine 32-bit EAX and EDX for 64-bit integer operations in 32-bit code)
- SI/ESI/RSI: Source index for string operations.
- DI/EDI/RDI: Destination index for string operations.
- SP/ESP/RSP: Stack pointer for top address of the stack.
- BP/EBP/RBP: Stack base pointer for holding the address of the current stack frame.
- IP/EIP/RIP: Instruction pointer. Holds the program counter, the current instruction address.
Segment registers:
* CS: Code
* DS: Data
* SS: Stack
* ES: Extra data
* FS: Extra data #2
由于是我们使用的32位的汇编指令,所以有个l
前缀,还有,和51单片机的堆栈不同,这里的堆栈是从高向低入栈的……还有一个问题就摘抄另外一个同学的文章吧,他说得很好
AT&T格式和intel格式,这两种格式GCC是都可以生成的,如果要生成intel格式的汇编代码,只需要加上
-masm=intel
选项即可,但是Linux下默认是使用AT&T格式来书写汇编代码,Linux Kernel代码中也是AT&T格式,我们要慢慢习惯使用AT&T格式书写汇编代码。这里最需要注意的AT&T和intel汇编格式不同点是:AT&T格式的汇编指令是“源操作数在前,目的操作数在后”,而intel格式是反过来的,即如下:
AT&T格式:movl %eax, %edx
Intel格式:mov edx, eax
表示同一个意思,即把eax寄存器的内容放入edx寄存器。这里需要注意的是AT&T格式的movl里的l表示指令的操作数都是32位,类似的还是有movb,movw,movq,分别表示8位,16位和64位的操作数。更具体的AT&T汇编语法请执行Google或者查阅相关书籍。
下面,我们开始反汇编一个C语言的程序,来分析一下它的汇编代码:
首先,我们先写一个C语言的程序main.c
int g(int x)
{
return x + 6;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(2333)+666;
}
在ubuntu平台下,使用 gcc -S -o main.s main.c -m32
将它反汇编成main.s
。注意,我是在AMD64(或者说X86-64)的操作系统,所以为了产生32位的汇编代码,我使用了-m32选项让它生成32位汇编指令
.file "main.c"
.text
.globl g
.type g, @function
g:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
movl 8(%ebp), %eax
addl $6, %eax
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size g, .-g
.globl f
.type f, @function
f:
.LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
pushl 8(%ebp)
call g
addl $4, %esp
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE1:
.size f, .-f
.globl main
.type main, @function
main:
.LFB2:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
pushl $2333
call f
addl $4, %esp
addl $666, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE2:
.size main, .-main
.ident "GCC: (Ubuntu 4.9.1-16ubuntu6) 4.9.1"
.section .note.GNU-stack,"",@progbits
代码中有许多以.
开头的代码行,属于链接时候的辅助信息,在实际中不会执行,把它删除,得到下列的代码就是纯汇编代码了:
g:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $6, %eax
popl %ebp
ret
f:
pushl %ebp
movl %esp, %ebp
pushl 8(%ebp)
call g
addl $4, %esp
leave
ret
main:
pushl %ebp
movl %esp, %ebp
pushl $2333
call f
addl $4, %esp
addl $666, %eax
leave
ret
下面,我们开始分析一下上面的汇编代码。
注意观察,每一个函数(在汇编,函数就是个代码段)的开头都是下面格式
函数名:
pushl %ebp
movl %esp, %ebp
;函数中间过程
leave(或者popl %ebp)
ret
注意,leave
和下面代码等价
movl %ebp, %esp
popl %ebp
也有时候,我们把下面代码写成enter
函数名:
pushl %ebp
movl %esp, %ebp
我们先分析一下这个函数执行的过程。
每次call一个函数,函数总是先把当前的栈底指针压入堆栈,然后把栈底指针移动到当前的栈顶,这样子做,相当于在旧的栈上新起了一个栈。然后在新栈上执行函数。
结束函数执行的时候,如果有堆栈变化,我们在写单片机汇编的时候,我们的习惯是一个函数有多少push就写多少pop,但是,由于我们新引进了一个寄存器,我们可以用movl %ebp, %esp
来瞬间恢复堆栈。当然,如果没有堆栈的变化,我们当然可以优化编译器把这句话去了。
这时候,马上就要ret飞回调用它的函数了……别急,我们还需要恢复栈底指针,否则回去的日子就难过了。于是popl %ebp
。然后如果可以的话,我们会用leave
来代替刚刚的两行代码。
函数执行一定得是有函数调用了。
pushl $2333
call f
addl $4, %esp
这是调用f(2333)函数的过程。
我们可以看到,我们把2333压栈,然后调用了f函数。
等到ret后,返回了现在的call的下一行汇编代码。这时候,esp和ebp是一个值,所以这以后如果压栈的时候,会覆盖了栈底指针,把esp往栈顶上移动1个单位也就是4个字节,这时候就完美解决了调用后的问题,才是真正调用完成了。
那么,怎么取得参数呢?
这时候,得回头看一下f函数了。这时候,我们发现它用了
pushl 8(%ebp)
call g
addl $4, %esp
它把增加了8个字节的地址压栈了,然后调用了g函数。
分析一下为什么是8个字节,我们可以用sizeof关键字来测试得到int占4个字节……所以,它却加了8个字节取值,那么必然是有什么怪东西又入栈了。pushl %ebp
是每次函数执行的时候使用的,o(∩_∩)o 哈哈,找到了,就是ebp寄存器还占用了4个字节,想想,32位芯片,寄存器
所以,又发现了ebp寄存器的一个好处,能够让我们方便取得函数的参数……否则后面再去参数,栈位置变了好多,就不方便了。
addl $6, %eax
之类的基本汇编指令,就不细说了,具体还是看看汇编的资料吧。知道一门汇编,就能很轻松看懂了。(上面的意思是把eax寄存器存的值加6)
enter
,函数执行后要leave
(如果没有改变esp就可以省去把ebp赋值给esp的步骤了),ret