此篇文章出于完成作业的目的,同时也总结一下自己的学习的体会,巩固一下学习成果。是完全真实的作业过程。如需转载请保留以下信息:
陈铁 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000。
今天计算机已经成为我们生活中重要不可分离的重要组成部分,从随身携带的手机到超级计算机,大部分都遵循冯诺伊曼体系结构:存储程序、顺序执行。程序编制好后,通过输入设备提供给计算机顺序执行。只要人可以将需要解决的问题描述为计算机可以顺序执行的指令序列,计算机就可以给出相应的结果。所以人们编制了计算机语言用来描述问题,现代计算机语言分为低级语言和高级语言,低级语言更接近机器,高级语言更接近人类。为了描述计算机的工作过程,我们采用接近机器的汇编语言(组合语言)描述计算机的执行过程。
实验环境的主机操作系统是Windows7 64位,运行VirtualBox 4.3.20 Edition,虚拟机安装CentOS7.0 64bit,Linux kernel 3.10.0。gcc版本4.8.2,gdb版本GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-51.el7。
以下是作业过程说明:
1.C语言源代码如下:
#include <stdio.h>
int g(int x) {
return x+2;
}
int f(int x) {
return g(x);
}
int main() {
return f(7)+5;
}
2.执行命令进行gcc -S main.s main.c生成汇编代码方便我们步进分析代码的执行情况。执行gcc -g main.c -o main生成可用gdb调试的执行代码。在linux终端下执行代码情况如下:
3.我们通过分析代码流程也可以得到正确答案:
程序从main开始执行,调用了f函数,把参数7传过去赋给x。f函数又调用了g函数,把x也就是7传过去,g函数得到参数x的值为7,返回7+2=9给f函数,f函数把9返回给main函数,main函数返回9+5=14作为程序的执行结果。在Linux终端下,14保存在系统变量$?中。
4.计算机系统我们可以抽象简化为CPU、内存、输入输出几部分。下面我们看一下这个程序在我的环境下,计算机是如何机械的的出这个14的。存在函数的程序会大量进行内存的堆栈操作,简单的加法运算在此不展开介绍,重点对于堆栈的操作进行跟踪。以下是cat main.s 所列出的汇编代码,仅保留可执行的部分。
g: pushq %rbp movq %rsp, %rbp movl %edi, -4(%rbp) movl -4(%rbp), %eax addl $2, %eax popq %rbp ret f: pushq %rbp movq %rsp, %rbp subq $8, %rsp movl %edi, -4(%rbp) movl -4(%rbp), %eax movl %eax, %edi call g leave ret main: pushq %rbp movq %rsp, %rbp movl $7, %edi call f addl $5, %eax popq %rbp ret |
(1)执行gdb main进入调试,l命令可以显示出C语言代码。break main设置断点,使程序直行到程序开始处停下来,然后单步执行,看一下计算机到底是如何工作的。run命令使程序开始执行。pushq %rbp;movq %rsp, %rbp后,此时rip指向rip=0x40051a。
(2)执行 info registers命令查看一下寄存器的情况。堆栈指针rbp和rsp指向相同的地址0x7fffffffe550,表明当前程序堆栈为空。
这时汇编代码保存了堆栈原来的指针,操作系统开始调用main函数。可以看到rip指向下一条要执行指令的地址。
(3)有函数调用,我们在gdb中执行stepi命令。执行movl $7, %edi。
(gdb) stepi (gdb) print $rip $1 = (void (*)()) 0x40051f <main+9> (gdb) print $edi |
(4)这时把程序中传给函数f的7保存进了寄存器edi中,继续执行,调用f函数。call f,执行的操作是当前rip=0x00400524值压栈(在gdb中可以执行x %rsp命令查看),rsp-8,f函数所在地址放入rip中。计算机会执行f函数中的pushq $rbp;movq %rsp,%rbp;subq $8, %rsp实际是保存调用f函数前main函数的指针。此时rbp=0x7fffffffe540,rsp=0x7fffffffe538;而(rbp)保存着调用前堆栈栈顶地址,当然栈顶移动8个字节用来接受传人的参数。
(5)执行3次stepi命令,movl %edi, -4(%rbp);movl -4(%rbp), %eax;movl %eax, %edi 这三行指令很明确,从main传过来的参数存入堆栈空间,然后通过eax寄存器保存一下,在此放到edi中,准备传给g函数。
(6)调用g函数时call g:rip值压栈;rsp-8;g地址赋给rip。
接下来执行两条初始化指令pushq %rbp;movq %rsp, %rbp,保存后rbp、rsp变成了0x7fffffffe528。movl %edi, -4(%rbp);movl -4(%rbp), %eax;addl $2, %eax,接受传入的参数,通过eax执行加法,此时结果保存在eax中。然后g函数执行恢复处理,popq %rbp;ret,rsp指向0x7fffffffe538。
(7)g函数返回时结果保存在eax中。回到f函数代码继续执行。其中leave指令相当于
movq %rbp, %rsp
popq %rbp
程序执行后寄存器情况如下:
ret指令执行后rsp=0x7fffffffe548
(8)完成f函数调用后,回到main函数执行addl $5, %eax语句。结果保存在eax中。
(9)后面代码是完成main函数返回,原理和一般函数调用相同,在此不在分析。
总结:在我最初接触计算机的时候,上机的机时还是很奢侈的东西,那时就给自己制定了学习方法:首先根据教材所教的知识,在头脑中模拟计算机会如何执行,假定课本上的例子都是正确的,推导出机器应当给出什么样的结果,当有机会在计算机上操作时再进行验证。现在看来,那时的思路是对的,但由于没能坚持,今天的计算机水平还是一般。就本质而言,今天的计算机的确就是模拟人的操作过程,程序员如何设计的程序,计算机就会不折不扣的执行。
本文出自 “StudyPark” 博客,请务必保留此出处http://swordautumn.blog.51cto.com/1485402/1618288
原文地址:http://swordautumn.blog.51cto.com/1485402/1618288