汇编语言入门
<h1>七、CPU 指令</h1><p>7.1 一个实例</p>
<p>了解寄存器和内存模型以后,就可以来看汇编语言到底是什么了。下面是一个简单的程序example.c。</p>
<blockquote>
<p>int add_a_and_b(int a, int b) { return a + b; }</p>
<p>int main() { return add_a_and_b(2, 3); }</p>
</blockquote>
<p>gcc 将这个程序转成汇编语言。</p>
<blockquote>
<p>$ gcc -S example.c</p>
</blockquote>
<p>上面的命令执行以后,会生成一个文本文件example.s,里面就是汇编语言,包含了几十行指令。这么说吧,一个高级语言的简单操作,底层可能由几个,甚至几十个 CPU 指令构成。CPU 依次执行这些指令,完成这一步操作。</p>
<p>example.s经过简化以后,大概是下面的样子。</p>
<blockquote>
<p>_add_a_and_b:</p>
<p>push %ebx</p>
<p>mov %eax, [%esp+8]</p>
<p>mov %ebx, [%esp+12]</p>
<p>add %eax, %ebx</p>
<p>pop %ebx</p>
<p>ret</p>
<p>_main:</p>
<p>push 3</p>
<p>push 2</p>
<p>call _add_a_and_b</p>
<p>add %esp, 8</p>
<p>ret</p>
</blockquote>
<p>可以看到,原程序的两个函数add_a_and_b和main,对应两个标签_add_a_and_b和_main。每个标签里面是该函数所转成的 CPU 运行流程。</p>
<p>每一行就是 CPU 执行的一次操作。它又分成两部分,就以其中一行为例。</p>
<blockquote>
<p>push %ebx</p>
</blockquote>
<p>这一行里面,push是 CPU 指令,%ebx是该指令要用到的运算子。一个 CPU 指令可以有零个到多个运算子。</p>
<p>下面我就一行一行讲解这个汇编程序,建议读者最好把这个程序,在另一个窗口拷贝一份,省得阅读的时候再把页面滚动上来。</p>
<p>7.2 push 指令</p>
<p>根据约定,程序从_main标签开始执行,这时会在 Stack 上为main建立一个帧,并将 Stack 所指向的地址,写入 ESP 寄存器。后面如果有数据要写入main这个帧,就会写在 ESP 寄存器所保存的地址。</p>
<p>然后,开始执行第一行代码。</p>
<blockquote>
<p>push 3</p>
</blockquote>
<p>push指令用于将运算子放入 Stack,这里就是将3写入main这个帧。</p>
<p>虽然看上去很简单,push指令其实有一个前置操作。它会先取出 ESP 寄存器里面的地址,将其减去4个字节,然后将新地址写入 ESP 寄存器。使用减法是因为 Stack 从高位向低位发展,4个字节则是因为3的类型是int,占用4个字节。得到新地址以后, 3 就会写入这个地址开始的四个字节。</p>
<blockquote>
<p>push 2</p>
</blockquote>
<p>第二行也是一样,push指令将2写入main这个帧,位置紧贴着前面写入的3。这时,ESP 寄存器会再减去 4个字节(累计减去8)。</p>
<p><img src="https://img-blog.csdnimg.cn/20201118101028374.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3d4aDAwMDBtbQ==,size_16,color_FFFFFF,t_70"></p>
<p>7.3 call 指令</p>
<p>第三行的call指令用来调用函数。</p>
<blockquote>
<p>call _add_a_and_b</p>
</blockquote>
<p>上面的代码表示调用add_a_and_b函数。这时,程序就会去找_add_a_and_b标签,并为该函数建立一个新的帧。</p>
<p>下面就开始执行_add_a_and_b的代码。</p>
<blockquote>
<p>push %ebx</p>
</blockquote>
<p>这一行表示将 EBX 寄存器里面的值,写入_add_a_and_b这个帧。这是因为后面要用到这个寄存器,就先把里面的值取出来,用完后再写回去。</p>
<p>这时,push指令会再将 ESP 寄存器里面的地址减去4个字节(累计减去12)。</p>
<p>7.4 mov 指令</p>
<p>mov指令用于将一个值写入某个寄存器。</p>
<blockquote>
<p>mov %eax, [%esp+8]</p>
</blockquote>
<p>这一行代码表示,先将 ESP 寄存器里面的地址加上8个字节,得到一个新的地址,然后按照这个地址在 Stack 取出数据。根据前面的步骤,可以推算出这里取出的是2,再将2写入 EAX 寄存器。</p>
<p>下一行代码也是干同样的事情。</p>
<blockquote>
<p>mov %ebx, [%esp+12]</p>
</blockquote>
<p>上面的代码将 ESP 寄存器的值加12个字节,再按照这个地址在 Stack 取出数据,这次取出的是3,将其写入 EBX 寄存器。</p>
<p>7.5 add 指令</p>
<p>add指令用于将两个运算子相加,并将结果写入第一个运算子。</p>
<blockquote>
<p>add %eax, %ebx</p>
</blockquote>
<p>上面的代码将 EAX 寄存器的值(即2)加上 EBX 寄存器的值(即3),得到结果5,再将这个结果写入第一个运算子 EAX 寄存器。</p>
<p>7.6 pop 指令</p>
<p>pop指令用于取出 Stack 最近一个写入的值(即最低位地址的值),并将这个值写入运算子指定的位置。</p>
<blockquote>
<p>pop %ebx</p>
</blockquote>
<p>上面的代码表示,取出 Stack 最近写入的值(即 EBX 寄存器的原始值),再将这个值写回 EBX 寄存器(因为加法已经做完了,EBX 寄存器用不到了)。</p>
<p>注意,pop指令还会将 ESP 寄存器里面的地址加4,即回收4个字节。</p>
<p>7.7 ret 指令</p>
<p>ret指令用于终止当前函数的执行,将运行权交还给上层函数。也就是,当前函数的帧将被回收。</p>
<blockquote>
<p>ret</p>
</blockquote>
<p>可以看到,该指令没有运算子。</p>
<p>随着add_a_and_b函数终止执行,系统就回到刚才main函数中断的地方,继续往下执行。</p>
<blockquote>
<p>add %esp, 8</p>
</blockquote>
<p>上面的代码表示,将 ESP 寄存器里面的地址,手动加上8个字节,再写回 ESP 寄存器。这是因为 ESP 寄存器的是 Stack 的写入开始地址,前面的pop操作已经回收了4个字节,这里再回收8个字节,等于全部回收。</p>
<blockquote>
<p>ret</p>
</blockquote>
<p>最后,main函数运行结束,ret指令退出程序执行。</p><br><br>
来源:https://www.cnblogs.com/liming-php/p/14267343.html
頁:
[1]