你知道你写的代码是怎样执行起来的吗

写这篇博客就是想要解释一个我自己在学习过程中比较困惑的问题:我们所写的C语言代码是如何在计算机中运行起来的?首先需要的知识储备就是计算机里有哪些硬件,这些硬件之间是怎么配合着执行一条指令的,然后再分析,我们所写的代码有哪些指令构成,这些指令是怎么执行的。

一、计算机的硬件结构

说明:这里cpu内我没有把寄存器单独画出来,而是将一部分主要的寄存器画在了各个硬件里 

总的来说:主存是用来存储数据的,运算器是用来执行算术逻辑运算的,控制器可以控制各个硬件,负责从内存中取出指令,对指令进行分析,从而指挥指令有条不紊的执行。

具体来看各个部件:

1、运算器:运算器里主要执行算术、逻辑运算的是ALU,而ACC、MQ、X都是寄存器,用来存储参与运算的操作数或者ALU执行运算后的结果。(具体哪些可以用来存结果,哪些可以用来存操作数等知识这里不赘述)

2、控制器:控制器里对指令进行分析进而控制运行的是CU,PC是程序计数器,存放的是下一条要执行的指令的地址,会自动+1,IR是指令寄存器,存放具体的指令

3、主存:也就是通常所说的内存,除了用于存储数据的存储单元以外,还有MAR和 MDR两个寄存器,要想从内存中读取数据,需要将数据的地址放进MAR中,内存再将MAR中地址对应的数据放进MDR中(写操作类似)

二、各个硬件是怎样配合着执行一条指令的 

首先,我们所写C/C++这样的代码会经过编译、链接产生一个可执行文件(二进制的机器指令)存在我们的磁盘中,在执行前会被加载到内存中(执行时二进制的机器指令和数据一样,都是存放在内存中的,每一条指令也有其对应的地址)

以执行这条指令为例,康康各个硬件执行时市怎样配合工作的:

y=a*b+c;

假设内存布局如下图:

与之对应得执行步骤为:

1、pc=0(指向第一条指令),将pc的内容赋给MAR寄存器,使得MAR=0,且控制器告诉内存,此次执行读操作

2、内存找到MAR存储的地址所对应的存储单元,将该单元内的数据放进MDR中,使得MDR=0000010000000101

3、MDR中所存储的内容赋给IR寄存器,使得IR=0000010000000101

前三步完成了取指令操作

4、IR将操作码部分(000001)传递给CU,由CU分析出该条指令是要进行取数

5、IR将操作数部分(0000000101)传递给MAR

6、内存找到与MAR存储的地址对应的存储单元,并将该存储单元的内容放进MDR,使得MDR=0000000000000010

7、将MDR中的内容放入ACC寄存器中

此时,第一条指令执行完,pc=1(指向第二条指令),下面的步骤与上面类似都是:取值、分析、在控制器的控制下执行,在此不再赘述。

三、函数栈帧的创建和销毁

上面分析了单独一条指令在计算机中是怎样执行的,但是我们平时所写的C代码都至少有一个主函数,函数在执行函数体指令之前,会创建函数栈帧,也就是说:我们所写的C代码,经过编译之后所产生的二进制代码,不仅仅包含函数体部分所对应的指令,还在函数体指令前面多出一部分指令,这一部分指令就是为函数创立栈帧用的,函数调用结束后创建的栈帧会自动销毁。VS编译器调试环境中的反汇编,可以通过汇编代码清楚的看到,函数栈帧创建和销毁动作是怎样完成的(本人使用的是VS2010,不同编译器可能存在略微差异)

下面将通过一个例子分析来说明,函数栈帧创建、销毁以及函数传参的过程:

 

 补充:push是压栈操作,pop是出栈操作,ebp是栈底指针,esp是栈顶指针(会自动+1),可以通过下图中调用堆栈窗口发现,main()函数是由_tmainCRTStartup()调用的。

假设 _tmainCRTStartup()调用main()函数前,栈底指针和栈顶指针如下图:

开始执行main()函数之后,首先执行的三步是(汇编指令前的地址是该条指令的地址):

 00641400  push        ebp  :将ebp内容压入栈帧
 00641401  mov         ebp,esp  :将esp内容放入ebp中
 00641403  sub         esp,0E4h :esp减去0E4h

执行完这3步之后,内存布局如下图

00641409  push        ebx  
0064140A  push        esi  
0064140B  push        edi  :三个压栈操作
0064140C  lea         edi,[ebp-0E4h] :将三个压栈操作之前的栈顶指针加载到edi中

执行完这4步之后,内存布局如下图

  

00641412  mov         ecx,39h  
00641417  mov         eax,0CCCCCCCCh  
0064141C  rep stos    dword ptr es:[edi] 

这3步的作用是将main函数栈帧空间内的数据都改为:0CCCCCCCCh,这也就是为什么我们定义一个局部变量,如果不进行初始化,里面的随机的为CCCCCCCC这样的值,执行完之后的内存如下图所示:

  

前面这些步骤执行完后,才开始真正执行我们所写的代码 (每句C代码下面是其对应的汇编代码)

int a=10;
0064141E  mov         dword ptr [a],0Ah  
    int b=20;
00641425  mov         dword ptr [b],14h  
    int c=0;
0064142C  mov         dword ptr [c],0  

这3步就是给a,b,c变量对应地址内的值改为变量的值(怎样为变量分配地址跟编译器有关,不同编译器可能不同,其实我的编译器两个变量之间地址是相差8个字节的,为了方便我画成了4个字节)

下面的代码是调用add函数,但是通过下面的汇编代码可以看出,在调用函数之前先传递参数,并且传参的顺序是由右向左

 mov         eax,dword ptr [b]  
 push        eax  
 mov         ecx,dword ptr [a]  
 push        ecx  

下面在调试环境下按F11逐语句执行发现:在进入 add函数之前,call指令会先把下一条指令的地址压栈,从我第一张截图可以看到,call指令的地址是:0064143B,call指令的下一条指令的地址是:00641440,也即call指令会把00641440这个地址压栈,然后进入add函数内,内存布局变为:

进入add函数后的汇编代码如下图: 

函数体内 步骤与进入main()函数时一致,这里return z 这个操作是把z变量的值放入eax寄存器中,执行完之后,内存布局如下图:

  

006413E1  pop         edi  
006413E2  pop         esi  
006413E3  pop         ebx :三个出栈操作 
006413E4  mov         esp,ebp  :栈空间销毁
006413E6  pop         ebp  :出栈,内容放进ebp
006413E7  ret  :来到call指令的下一条指令(之前将call指令的下一条指令地址压栈就是为了函数调用结束后能返回)

上述代码执行完之后的内存布局变为:

紧接着回到call指令的下一条指令依次执行,本代码中下一条指令为:

add         esp,8 :销毁形参

mov         dword ptr [c],eax:将eax中存放的z的值,放入变量c中

执行之后内存布局为:

接下来就是main()执行结束后,栈空间的销毁,跟前面add函数栈空间销毁的步骤一样,就不再赘述啦。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 14
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值