简介: 本文将对 Linux™ 系统计算机的工作机制进行简单概述。文中将为您提供单任务系统如何工作的细节,然后将进一步展示汇编指令在CPU的运行过程,以及由单任务向多任务处理的扩展。
♦ gcc用法参考(*表示文件名)
– gcc –E –o *.cpp *.c 生成预处理文件
– gcc –x cpp-output –S –o *.s *.cpp
• gcc –S –o *.s *.c 编译成汇编
– gcc –x assembler –c *.s -o *.o
• gcc –c *.c -o *.o 汇编成目标代码
• as –o *.o *.s
– gcc –o * *.o
• gcc –o * *.c 生成可执行ELF文件格式
测试用源码如下,该源码不涉及系统调用,仅适用于用户态下CPU、堆栈处理流程分析;
int g(int x){
return x+3;
}
int f(int x){
return g(x);
}
int main(void){
return f(8)+1;
}
由于该源码未使用宏指令和#include指令,所以预处理后的文件内容没有明显变化;
使用命令“gcc -E lab.c -o lab.cpp”,以下为lab.cpp中内容:
# 1 "lab.c"
# 1 "<命令行>"
# 1 "lab.c"
int g(int x){
return x+3;
}
int f(int x){
return g(x);
}
int main(void){
return f(8)+1;
}
使用GCC编译器产生预处理文件的汇编代码:
使用命令“gcc -S lab.cpp -o lab.s”,以下为lab.s中内容:
.file "lab.cpp"
.text
.globl _Z1gi
.type _Z1gi, @function
_Z1gi:
.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 $3, %eax
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE0:
.size _Z1gi, .-_Z1gi
.globl _Z1fi
.type _Z1fi, @function
_Z1fi:
.LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $4, %esp
movl 8(%ebp), %eax
movl %eax, (%esp)
call _Z1gi
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE1:
.size _Z1fi, .-_Z1fi
.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
subl $4, %esp
movl $8, (%esp)
call _Z1fi
addl $1, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE2:
.size main, .-main
.ident "GCC: (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3"
.section .note.GNU-stack,"",@progbits
使用已有的汇编代码,生成可执行程序:
将汇编代码汇编为可执行代码:
1 执行"gcc -c example.s -o example.o"命令生成.o文件。
将目标代码转换成可执行ELF文件
2 执行"gcc example.o -o example"命令生成可执行文件。
上图为生成所有文件的索引及可执行文件的详细信息。
操作环境:edb
32位CPU所含有的寄存器有:
指针寄存器(ebp):指向当前所用栈的基址;
指针寄存器(esp):指向当前所用栈的顶部;
数据寄存器(eax、ebx、ecx和edx) :通常用来保存API的返回值,由于操作的效率比较高,因而使用的频率也比较高;
变址寄存器(esi和edi):主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便;
段寄存器(es、cs、ss、ds、fs和gs) :段寄存器是根据内存分段的管理模式而设置的,内存单元的物理地址由段寄存器的值和一个偏移量组合而成的,这样可用两个 较少位数的值组合成一个可访问较大物理空间的内存地址;
指令指针寄存器(eip):记录的是CPU将要执行的下一条指令地址;
标志寄存器(eflags):记录各种标志位;
使用EDB打开lab可执行文件(File->Open),之后可以看到如下画面。
edb调试可根据每一步指令,进行debug,上图可以看出初始化可执行文件后寄存器、堆栈的值及所有可执行汇编指令,在调试过程中可以观察当前每步过程中寄存器的变化,其中0xbfa9397c是程序执行时esp的值,也就是当前栈顶指针位置.ebp寄存器为0,eip初始值为0804840a,说明程序的执行的第一条指令的地址为0X0804840a。在堆栈区跟踪esp地址到达栈顶,值为0xb75d2935.
分析main函数执行过程:
初始: pushl %ebp: movl %esp, %ebp:
刚开始栈顶指针esp指向初始栈顶位置,然后将之前的栈底指针ebp入栈并且新的ebp和esp同时指向了地址减少4字节后的位置即第三个图中所示,很明显这一步的作用就是保存之前栈环境的前提下建立新的堆栈框架,堆栈变化如上图所示。
subl $4, %esp : movl $8, (%esp): call _Z1fi:
将esp向低地址增长4字节并将8入栈,CPU执行call指令时通常做两步操作,第一步将eip压栈做保存,第二步跳转,堆栈变化如图所示。
调用f(int)函数:
pushl %ebp: movl %esp, %ebp: subl $4, %esp:
将当前eip入栈并跳转到f中去。跳到f后做的第一件事儿仍然是保存之前栈环境并建立新的堆栈环境,将esp增长4位,指向下一地址,堆栈变化如上图。
movl 8(%ebp), %eax: movl %eax, (%esp): call _Z1gi:
将ebp增加8位的地址中的参数传入eax中,并将eax的值压入栈,调用call指令,将eip值压入栈,调转入g函数,堆栈变化如上图。
分析g函数的执行活动:
pushl %ebp: movl %esp, %ebp: movl 8(%ebp), %eax:
ebp压栈,初始化栈环境,将之前保存的变量再次转移到eax中,堆栈变化如上图。
addl $3, %eax: popl %ebp: ret:
将eax加3,使得eax中的值变为11。然后ebp出栈,返回到上次保存的ebp地址,esp逐次返回栈底,堆栈变化如上图。
函数执行结束后,指令执行过程分析:
leave: ret: addl $1, %eax: leave: ret: 执行leave操作ebp返回上次保存的地址,esp指向下一栈顶,ret操作esp逐次返回栈底,add操作使得eax再加1,生成新的值12,再次leave退出该进程的栈空间,eip返回到初始时候的值,至此,程序执行完毕,最终的结果保存在eax寄存器中。。
三、Linux计算机工作机制分析 单任务 在linux计算机中,我们讨论最简单的单个任务模型,即处理是顺序的不考虑中断和多任务情况下的工作过程。首先由单一进程开始执行,所需要的可执行代码均存储在代码区,而每个进程会专门开辟一段堆栈来支持该进程运行,该栈的增长方向是从高地址向低地址增长,栈中保存程序的局部变量,以及函数的返回地址等信息。进程执行过程中堆栈的控制由栈顶指针寄存器esp与栈基址寄存器ebp完成。 单个任务执行过程中,堆栈的控制单元按执行函数来分配,进制的每个函数都特定的栈空间,函数中的数据操作结果保存在数据寄存器(前文有介绍)中,从不同的函数进入其他函数,需要call指令来执行对应函数多在代码段的地址,在执行过程中程序会先初始化该函数的栈空间,保存之前的现场,例如eip,ebp等寄存器的值。一般eip来确定所要执行的指令的地址即压栈之前所在函数的入口地址。在新的栈空间中按esp来确定栈空间的大小。 根据前面任务执行过程的分析,在调用函数f或者g时,先将函数参数从数据寄存器中压入堆栈,然后调用call,转入对应函数空间,并将eip与ebp保存,eip为前一函数的call调用的操作数被保存,最后将esp,ebp重新指向新的栈基址,从而完成栈的初始化操作。 函数执行结束时一般都会执行ret和leave指令,leave指令一般就是返回上级栈的ebp与esp,ret指令是讲eip赋值为返回地址的值,使得函数结束后能够返回到调用该函数的父函数,从而使得单一进程能够在多个函数调用过程中顺序执行并能够在执行结束后及时的销毁内存空间并结束进程。
多任务 多任务系统执行原理就是所有的应用程序以进程的方式以较低级别运行在Linux操作系统中,每个进程有独立的堆栈空间,代码段和静态存储区,CPU在执行进程的时候有操作系统统一调度,所有执行进程按优先级的高低分配获得CPU。 由于多任务处理过程类似单任务处理过程中各函数间转换的过程,多任务系统中引入了中断机制。任何进程在运行时,有新的优先级更高的进程进入执行状态时,操作系统会发送一个中断信号到CPU,使得CPU转而去运行正常的控制流之外的进程代码。由于操作系统中存在多个进程,在进程优先级一直的情况下,所有任务如单任务系统中执行机制一样顺序的执行完成,当优先级不一致时,各个进程由操作系统统一调度,当调用一个进程时,我们将前一任务相关的执行信息(如esp,eip,任务执行的断点,当前的程序flag寄存器等等)保存起来,当该进程执行完毕后再去执行之前所未完成的进程,从而实现多任务的切换与分时控制。