计算机组成原理(五)-一条指令是怎么被执行的

什么是指令:

程序代码的本质就是一条一条的指令,我们需要通过编码的方式让CPU知道我们需要它干什么,最后由译码器翻译成一条条的机器指令。机器指令主要有两部分组成:操作码、地址码。地址码直接给出操作数和操作数的地址,分三地址指令、二地址指令和一地址指令,最后还有零地址指令,零地址指令在机器指令中没有地址码,用来进行空操作、停机操作、中断返回操作等。

计算机组成原理(五)-一条指令是怎么被执行的

 

那么一条简单的指令执行,涉及到了那些组件?

控制器(CU)和运算器(ALU)

PC寄存器(程序计数器):用于存放下一条需要执行的指令地址信息,注意是下一条指令的地址!

指令寄存器:用于存放当前正在执行的指令,注意是当前的指令!

指令译码器:用于翻译指令信息,与指令寄存器相连

计算机组成原理(五)-一条指令是怎么被执行的

 

那么一条指令的执行就应该包含

计算机组成原理(五)-一条指令是怎么被执行的

 

1.取指令:CU根据PC寄存器中的指令地址,去内存或者指令缓存中获取具体的指令,并加载到指令寄存器中。

2.分析指令:拿到指令之后,指令译码器将指令寄存器中的指令进行翻译,向ALU发起控制信号和指令信息。此时PC寄存器自增即+1,加载下一个要执行的指令地址(至少此时是这样的,此为顺序寻址)。

3.执行指令:ALU拿到相关信息之后进行相关的计算。

4.访问内存(或缓存):如果涉及到相关数据需要去缓存中拿,那就去取数据。

5.数据写回:将计算结果写入到寄存器中,写入到缓存中,甚至写入到内存中。

 

看到这里你肯定会想,并不是所有的指令都是顺序执行的,比如if····else,比如方法调用,此时PC寄存器也是自增加1嘛?我们来讲讲if····else和方法调用。

先来看if····else,下边有一小块代码

int r = 1;
int a = 10;
  if (r == 0)
  {
    a = 1;
  } else {
    a = 2;
  } 

很简单的一个if判断以C语言为例 if部分翻译为汇编代码就是

if (r == 0)
  3b:   83 7d fc 00             cmp    DWORD PTR [rbp-0x4],0x0
  3f:   75 09                   jne    4a <main+0x4a>
    {
        a = 1;
  41:   c7 45 f8 01 00 00 00    mov    DWORD PTR [rbp-0x8],0x1
  48:   eb 07                   jmp    51 <main+0x51>
    }
    else
    {
        a = 2;
  4a:   c7 45 f8 02 00 00 00    mov    DWORD PTR [rbp-0x8],0x2
  51:   b8 00 00 00 00          mov    eax,0x0
    } 

可以看到,这里对于 r == 0 的条件判断,被编译成了 cmp 和 jne 这两条指令。

这两条指令中cmp表示比较的意思,这明显是一条二地址指令,cmp是操作码,后边两个是地址码,意思就是将rbp寄存器中偏移量为4的位置的数取32位(DWORD PTR)出来,和0(0x0)比较,并将比较结果存到条件码寄存器中。

跟着的 jne 指令,是 jump if not equal 的意思,它会查看对应的零标志位。如果为 0,会跳转到后面跟着的操作数 4a 的位置。这个 4a,对应这里汇编代码的行号,也就是上面设置的 else 条件里的第一条指令。当跳转发生的时候,PC 寄存器就不再是自增变成下一条指令的地址,而是被直接设置成这里的 4a 这个地址。

那么CPU 再把 4a 地址里的指令加载到指令寄存器中来执行。mov 指令把 2 设置到对应的寄存器里去,相当于一个赋值操作。然后,PC 寄存器里的值继续自增,执行下一条 mov 指令。

最后一条mov指令的第一个操作数 eax,代表累加寄存器,第二个操作数 0x0 则是 16 进制的 0 的表示。这条指令其实没有实际的作用,代表我执行完了,给 main 函数生成了一个默认的为 0 的返回值到累加器里面。

然后是if条件之后我们注意到有个jmp指令,它之前的mov指令和4a是一样的意思,jmp指令,也就是jump的缩写,这是一个无条件跳转指令。跳转的地址就是这一行的地址 51,也就是说这个指令同样会改变PC寄存器中的地址信息,即将PC寄存器中的值设置为51(51大家都能使用,没必要生成两条指令)。具体流程如图所示,for循环也是同样的道理,不过涉及到具体的指令意义需要大家自己探索,但是流程是一样的,都是通过指令改变PC寄存器中的指令地址,引导CPU执行自己想要执行的指令。

计算机组成原理(五)-一条指令是怎么被执行的

 

下面是方法调用

方法调用之前,我们先了解一个数据结构-栈

是内存中一段连续的物理地址组成的一个后进先出(LIFO)的数据结构,这种栈结构大小确定,满了就溢出,所以底部确定,被称为栈底,我们向里边写入数据被称为压栈(PUSH),读取数据被称为出栈(POP),我们每次POP都是拿栈的最后一个数据,被称为栈顶,栈底地址最大,因为每次从栈顶拿数据,可以减少寻址时间。

计算机组成原理(五)-一条指令是怎么被执行的

 

函数 A 在调用 B 的时候,需要传输一些参数数据,这些参数数据在寄存器不够用的时候也会被压入栈中。整个函数 A 所占用的所有内存空间,就是函数 A 的栈帧(Stack Frame)。

以一段程序作为举例

int static add(int a, int b)
{
    return a+b;
}
 
 
int main()
{
    int x = 5;
    int y = 10;
    int u = add(x, y);
}

汇编为

int static add(int a, int b)
{
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   89 7d fc                mov    DWORD PTR [rbp-0x4],edi
   7:   89 75 f8                mov    DWORD PTR [rbp-0x8],esi
    return a+b;
   a:   8b 55 fc                mov    edx,DWORD PTR [rbp-0x4]
   d:   8b 45 f8                mov    eax,DWORD PTR [rbp-0x8]
  10:   01 d0                   add    eax,edx
}
  12:   5d                      pop    rbp
  13:   c3                      ret    
0000000000000014 <main>:
int main()
{
  14:   55                      push   rbp
  15:   48 89 e5                mov    rbp,rsp
  18:   48 83 ec 10             sub    rsp,0x10
    int x = 5;
  1c:   c7 45 fc 05 00 00 00    mov    DWORD PTR [rbp-0x4],0x5
    int y = 10;
  23:   c7 45 f8 0a 00 00 00    mov    DWORD PTR [rbp-0x8],0xa
    int u = add(x, y);
  2a:   8b 55 f8                mov    edx,DWORD PTR [rbp-0x8]
  2d:   8b 45 fc                mov    eax,DWORD PTR [rbp-0x4]
  30:   89 d6                   mov    esi,edx
  32:   89 c7                   mov    edi,eax
  34:   e8 c7 ff ff ff          call   0 <add>
  39:   89 45 f4                mov    DWORD PTR [rbp-0xc],eax
  3c:   b8 00 00 00 00          mov    eax,0x0
}
  41:   c9                      leave  
  42:   c3                      ret    

这里需要先介绍几个专用的寄存器,简要介绍如下:

  • ax(accumulator): 可用于存放函数返回值
  • bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址
  • sp(stack poinger): 用于存放执行中的函数对应的栈帧的栈顶地址
  • ip(instruction pointer): 指向当前执行指令的下一条指令

从汇编代码中可以看出一个函数被call调用,首先默认要完成以下动作:

   push   rbp
   mov    rbp,rsp

1.将调用函数的栈帧栈底地址入栈,即保存调用函数的栈帧的栈底地址

2.建立新的栈帧,把 rsp 这个栈指针(Stack Pointer)的值复制到 rbp 里,并保存rsp的值,而 rsp 始终会指向栈顶。这个命令意味着,rbp 这个栈帧指针指向的地址,变成当前最新的栈顶。

而调用结束栈帧出栈,跳转到方法调用方的调用位置的下一个位置,即call的下一行继续执行。

//以下两步等同于leave
mov    rsp,rbp
pop    rbp
//以下两步等同于ret
pop rip
jmp rip

所以一个完整的方法调用过程

调用方使用call指令调用方法,此时rip中存放的是call的下一条指令的地址,将rip压栈到栈底,生成新的栈帧,将栈帧压栈,执行方法,栈帧弹出,弹出rip,跳转到rip继续运行调用方的方法。

 

 

 
上一篇:如何绕过云文件共享服务基准测试陷阱


下一篇:re | [NPUCTF2020]BasicASM