陈民禾,原创作品转载请注明出处《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000,我的博客中有一部分是出自MOOC课程中视频,再加上一些我自己的理解。
一、首先在理解计算机是如何工作之前,我们先来理解一下现代计算机的模型。
现代计算机是存储程序计算机,依据冯诺依曼体系结构构造。从硬件的层面,也就是我们从计算机的主板。冯诺依曼体系结构我们可以大概抽象成一个cpu。还有一块可以抽象成内存,cpu和内存有一个连接,我们称之为总线,然后就是cpu内部,cpu里面有个很重要的寄存器,叫做ip,它总是指向内存的某一块区域,比如说它指向代码段,此时cpu就从IP指向的那个内存地址取过来一条指令执行,执行完之后Ip自加一,这样像贪吃蛇一样一步步向下面走,这就是从硬件方面解释的冯诺依曼体系。
二、计算机依靠机器语言进行工作,我们可以通过汇编语言来“指挥”计算机完成一些工作,下面就是一些汇编基础知识,总结如下:
X86计算机的寄存器:32位的X86计算机的寄存器,它的低16位作为16位的寄存器,16位的寄存器,它还有8位的寄存器。8位的寄存器包含AH,BH,CH,DH,AL,BL,CL,DL。16位的寄存器包含AX,BX,CX,DX,BP,SI,DI,SP。所有开头为E的寄存器一般来讲是32位的。譬如EAX:累加器,EBX:基地址寄存器,ECX:计数寄存器,EDX:数据寄存器,EBP:堆栈基指针(比较重要),ESI、EDI:变址寄存器。ESP:堆栈顶指针。这里会有一个比较重要的概念,就是堆栈的概念。压栈、出栈分别是push指令和pop指令。还有各种概念,CS:代码段寄存器,DS:数据段寄存器,ES、FS、GS:附加段寄存器,SS:堆栈段寄存器,其中代码段和堆栈段我们用的比较多,我们在使用某一条指令的时候,实际上我们是用CS加上EIP来描述,我们要知道代码的代码段还有这个地址EIP。还有一个标志寄存器:EFLAG(可以用于标示当前的一些状态)。
三、下面是举例的一些汇编指令,这些汇编指令有助于理解汇编代码中堆栈的使用和几种寻址方式:
这里我们讲一些常见的指令,我们最常见的就是movl指令,也就是L是指32位。
首先:%开头标示寄存器,$开头表示数值。 寄存器寻址:寄存器寻址指的是被操作的都是寄存器、和内存不打交道。movl%eax,%edx;表示操作的都是寄存器。movl就是把前面eax的内容放到后面edx里面,实际上它就相当于后面的把eax赋值给edx。
立即寻址: movl $0x123 ,edx edx=0x123;,这个$符号表示16进制数字所表示的数值。把数值直接放到edx寄存器里面,这就叫做立即寻址。立即寻址也和内存没有关系,它只是把一个绝对的数放到里面。
直接寻址:直接访问一个指定的内存地址的数据movl 0x123,%edx edx=*(int 32_t*)0x123;把内存地址123所指向的那块内存数据,把数值强制转换成32位的int变量的指针,我再用一个*来取它的值,然后赋值给edx,这叫做直接寻址。
间接寻址:将寄存器的值作为一个地址来访问内存,movl (%ebx),%edx edx=*(int 32_t*)ebx ;%ebx就是指ebx寄存器里面存的值,这个存的值就是一个内存地址,加括号代表内存地址里面所存的值,把这个值赋值给edx。
变址寻址:在间接寻址之时改变寄存器的数值:movl 4(%ebx),%edx edx=*(int 32_t*)(ebx+4);就是在间接寻址的基础上,在地址加4的基础上,将内存地址上面存的数赋值给edx。
有几个常见的汇编指令:push、pop、call、ret:
pushl %eax(其中l表示32位):pushl %eax也就是我们把%开头的eax这个寄存器压栈到堆栈的栈顶,实际上它做了两个动作,第一个就是把堆栈的esp,压栈的话是首先让esp减去4,然后把eax放到esp减去4后的内存位置,这样是向下增长的。
两条指令分别是:subl $4,%esp movl %eax,(%esp)
popl %eax:出栈指令,把eax就是从堆栈的栈顶,同样对应着两个动作首先把堆栈的栈顶数值存放到eax里面,然后再把栈顶加四。相当于两条指令分别是:movl (%esp),%eax addl $4,%esp
call:函数调用指令 call 0x12345,函数调用堆栈是理解C代码在cpu上执行的关键,也可以把这句话替换成为两句话,pushl %eip(*) movl $0x12345 ,%eip(*)
第一句话是把eip给压栈,然后把立即数赋值给eip,这句话中首先eip是当前cpu的相当于指令指针。我们可以把当前的eip压栈,下一次cpu执行新指令的时候就从这个位置开始取,
ret:return指令
可以将其替换成为pop %eip(*),加*号的意思是这些指令是不可以被直接使用的是伪指令,注意:eip寄存器不能被直接修改,只能通过特殊指令间接修改,因为这样可能带来安全隐患。
四、最后也是也是这篇博客最主要的部分,以我们做的实验为例,分析一下这个程序代码中计算机是如何工作完成这项工作的,其中如果有不正确的地方,希望大家帮忙指正。
实验大致步骤如下:
在实验楼中打开code,在下面写程序,ls命令然后使用vi main.c vi命令用于新建文件并将光标置于第一行行首
然后是复制粘贴上我们预先完成的c语言代码:
按下esc,:输入wq退出
之后可以gcc main.c编译文件生成一个./a.out的文件,如果要将它改成一个汇编代码的话、有一个命令gcc -S -o main.s(.s作为汇编代码)main.c -m32
此时打开vi main.s可以看到只需要关心的是把.开头的用于链接的语句都删掉,留下来的就是纯汇编代码
下面是c语言部分代码:
int g(int x)
{
return x+3;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(11)+1;
}
下面是汇编部分代码堆栈的变化过程,我画在一张纸上:
下面还有:
执行过程分析:
eip指令:在函数中总是向下执行,如果由call语句,那么eip的值将会被修改。
函数调用堆栈是由逻辑上多个堆栈叠加起来的,esp指向堆栈的栈顶,ebp指向堆栈的栈底。
eax:函数的返回值默认使用eax寄存器存储返回给上一级函数,暂时存储一些数值,
g:
pushl %ebp
movl %esp,%ebp
movl 8(%ebp),%eax
addl $3,%eax
popl %ebp//将ebp的值放回,这样ebp又指向原来4的位置
f:
pushl %ebp//将ebp来pop进来
movl %esp,%ebp//把esp的值赋值给ebp
subl $4,%esp//把esp的值减去4
movl 8(%ebp),%eax//把ebp变址寻址加上8,这时候eax也就等于8
movl %eax,(%esp)//把esp的值赋值给eax
call g//eip指向下一条指令,同时esp加上4
leave//// mov %ebp,%esp,pop ebp
ret//pop掉eip
main://首先程序从main函数开始执行
pushl %ebp//压栈将ebp放入同时esp的值被修改
movl %esp,%ebp//ebp的内容发生改变,也指向1的位置
subl $4,%esp//第三条指令执行,esp减去4
movl $8,(%esp)//将5放入当前指向的esp所想用的内存中
call f//push eip move f,eip执行这个动作时指向下一个eip地址,专项调用f函数
addl $1,%eax//eax作为默认的返回值
leave// mov %ebp,%esp,pop ebp
ret//回到初始main函数时的堆栈
整个过程是堆栈的变化
五、总结
在学习这个课程的过程中了解了计算机是怎样工作的。在现代计算机中程序和数据都存储在计算机当中,计算机有它自己的语言,就是汇编语言,它是一门更贴近于机器的语言。
我理解的计算机进行工作时,总线连接cpu和内存,cpu内有许多的寄存器,他们分工不同,大小也不同,cpu其中有个很重要的寄存器,就是ip寄存器,ip寄存器不断向下递增,指令被不断地执行。计算机通过一定的语句来控制这些组件,实现计算机要完成的工作,就拿这个实验,将c语言编译成机器更容易理解的汇编语言。使用汇编语言,指令被不断地执行,反映在栈中,就是堆栈里面不断发生改变,计算机这样完成了自己的工作。
这就是我完成这次作业的感想。