linux内核分析课程笔记(一)
冯诺依曼体系结构
冯诺依曼体系结构实际上就是存储程序计算机。
从两个层面来讲:
从硬件的角度来看,冯诺依曼体系结构逻辑上可以抽象成CPU和内存,通过总线相连。CPU上有一些寄存器,IP(Instruction Pointer)是一个指针,总是指向内存的某一块区域CS(Code Segment),CPU即从IP指向的地址取一条指令进行执行,执行完之后IP自增1,加到下一条指令(逻辑意义上的1,因为有些指令系统是变长指令)
从程序员的角度来看,存储程序计算机。CPU从程序员的角度可以抽象成一个无条件的for循环,总是执行下一条指令。
for(;;){
next instruction();
}
API(Application Program Interface)
ABI(Application Binary Interface)
32位的x86中EIP的三条特性:
- 每条指令执行完成后EIP自加1
- 指令的长度不同
- EIP还可能会被一些指令修改,如:CALL、RET、JMP以及条件JUMP
x86汇编基础
x86寄存器
有8位、16位和32位的寄存器。
16位:AX、BX、CX、DX、BP、SI、DI、SP
32位:EAX(累加器)、EBX(基地址寄存器)、ECX(计数寄存器)、EDX(数据寄存器)、EBP(堆栈基址指针)、ESI与EDI(变址寄存器)、ESP(堆栈顶指针)
段寄存器:
- CS(Code Segment Register),代码段
- DS(Data Segment Register),数据段
- ES(Extra Segment Register),附加数据段
- SS(Stack Segment Register),堆栈段
- FS与GS段寄存器用于区分x86的两种模式
CPU在实际取指令时根据CS:EIP来准确定位一个指令。
x86指令
mov指令与寻址方式
- 寄存器寻址
movl %eax,%edx(%为寄存器标识,操作均为寄存器,与内存无关)
执行结果:edx = eax
- 立即寻址
movl $0x123,%edx ($表示数值,表示将立即数存入寄存器中)
执行结果:edx = 0x123
- 直接寻址
movl 0x123,%edx (将某个内存地址的内容存入寄存器中)
执行结果: edx = *(int32_t*)0x123
这里将0x123当作指针,寻找其指向的内存内容
- 间接寻址
movl (%ebx), %edx (将寄存器中存放的值作为内存地址,取出其中内容,使用()表示**从地址取出内容**)
执行结果: edx = *(int32_t*)ebx
- 变址寻址
movl 4(%ebx),%edx (将寄存器中存放的值作为基内存地址,加入偏移量后再取出相应内存地址对应内容)
执行结果: edx = *(int32_t*)(ebx+4)
b,w,l,q分别代表8位,16位,32位和64位
(样例中是movl
,即表示是32位mov指令)
对于x86来说,大部分指令都能直接访问一个内存地址。
AT&T汇编格式与Intel汇编格式略有不同,Linux内核使用的是AT&T汇编
PUSH、POP、CALL和RET
pushl %eax
拆解指令:
subl $4,%esp
movl %eax,(%esp)
含义:将栈顶指针减4(栈从高到低增长),然后将eax里的值放入现在的栈顶所指向的内存中。
popl %eax
拆解指令:
movl (%esp),%eax
addl $4,%esp
含义:将栈顶值放入eax寄存器中,栈顶指针+4表示退栈
call 0x12345
拆解指令:
pushl %eip (当前的eip压栈,即要保存返回地址)
movl $0x12345,%eip (将 eip = 0x12345,即CPU要执行的下一条指令是0x12345指向的指令)
ret
拆解指令:
popl %eip (将call保存的eip还原到eip寄存器中)
约定
写汇编代码的时候不能直接修改eip寄存器,所以形如movl $8,%eip
的用法是错误的!
C到汇编
enter:
pushl %ebp
movl %esp,%ebp
建立堆栈
leave:
movl %ebp,%esp
popl %ebp
退栈
实验截图
以下是我的实验截图,因为在学校学号是13061193,所以使用了这个数字作为个人的特殊标识:
函数调用堆栈分析
一些知识点
- 函数的调用堆栈是由逻辑上的多个堆栈叠加起来形成的。
- 函数的返回值默认使用eax寄存器存储返回给上一级函数,跟MIPS中的$v0寄存器的作用一样。
汇编代码分析
int g(int x)
{
return x + 3;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(13061193) + 1;
}
首先看最简单的 int g(int x)
函数,它不涉及到嵌套函数调用,只有一条语句用于返回 x+3
。
g函数编译成的汇编代码段即:
pushl %ebp
movl %esp,%ebp
movl 8(%ebp),%eax
addl $3,%eax
popl %ebp
ret
第1、2行汇编代码等价于enter
,是一个存储函数基地址的过程。而在存储函数基地址之前,实际上还需要存储的是函数的参数与返回地址。返回地址的存入不是显式的,是在call语句时eip被存入栈中。
从开始到结束,整个过程可以用如下动图所示:
- 左侧堆栈为执行完红色指令后的堆栈状态。
- 使用main-eip、f-eip的意思是指,函数调用的返回点分别是在main、f函数中。
- 使用main-ebp、f-ebp、g-ebp的意思是 这些分别是三个函数的基地址。
- f-x 和 g-x 的意思是 这两个函数的参数 x 。
总结
冯诺依曼结构下的计算机是依靠CPU执行存储器中的代码段,配合存储器中的数据段等来工作的。