<h1>一次彙編分析的經歷</h1>
<h3>關鍵詞(爲了搜索引擎優化,爲了點擊量)</h3>
<p><strong>寄存器</strong>、<strong>ia32</strong>、<strong>AT&T彙編</strong>、<strong>intel彙編</strong>、<strong>x86架構</strong>、<strong>x64架構</strong></p>
<h3>背景</h3>
<p>爲了想知道爲什麼i++ ++i不是原子操作</p>
<h3>測試方法</h3>
<p>編寫兩個程序,調用<code>objdump -d</code>對比彙編代碼</p>
<h4>程序1</h4>
<pre><code>int main()
{
int i = 0;
int a = i++;
return 0;
}
</code></pre>
<h4>程序1的彙編代碼</h4>
<pre><code>00000000004005dc <main>:
4005dc: 55 push %rbp
4005dd: 48 89 e5 mov %rsp,%rbp
4005e0: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp)
4005e7: 8b 45 f8 mov -0x8(%rbp),%eax
4005ea: 89 45 fc mov %eax,-0x4(%rbp)
4005ed: 83 45 f8 01 addl $0x1,-0x8(%rbp)
4005f1: b8 00 00 00 00 mov $0x0,%eax
4005f6: c9 leaveq
4005f7: c3 retq
</code></pre>
<h4>程序2</h4>
<pre><code>int main()
{
int i = 0;
int a = ++i;
return 0;
}
</code></pre>
<h4>程序2的彙編代碼</h4>
<pre><code>00000000004005dc <main>:
4005dc: 55 push %rbp
4005dd: 48 89 e5 mov %rsp,%rbp
4005e0: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%rbp)
4005e7: 83 45 f8 01 addl $0x1,-0x8(%rbp)
4005eb: 8b 45 f8 mov -0x8(%rbp),%eax
4005ee: 89 45 fc mov %eax,-0x4(%rbp)
4005f1: b8 00 00 00 00 mov $0x0,%eax
4005f6: c9 leaveq
4005f7: c3 retq
</code></pre>
<p>兩坨彙編的不同之處:</p>
<pre><code>4005e7: 8b 45 f8 mov -0x8(%rbp),%eax
4005ea: 89 45 fc mov %eax,-0x4(%rbp)
4005ed: 83 45 f8 01 addl $0x1,-0x8(%rbp)
4005e7: 83 45 f8 01 addl $0x1,-0x8(%rbp)
4005eb: 8b 45 f8 mov -0x8(%rbp),%eax
4005ee: 89 45 fc mov %eax,-0x4(%rbp)
</code></pre>
<p>of course 我們知道:</p>
<ol>
<li>i++ 是先取值再自增</li>
<li>++i 是先自增再取值</li>
</ol>
<p>所以addl一個在前一個在後</p>
<p>另外,一行i++彙編出這麼多條指令導致i++必然不是原子</p>
<h2>細節分析</h2>
<p>當然不能這麼簡單的放過這段代碼,前面後面那一坨子東西是幹嘛的?</p>
<h4>疑點</h4>
<ol>
<li>rbp rsp eax是什麼</li>
<li>movl是什麼</li>
<li>$0x0是什麼</li>
<li>-0x8(%rbp)是什麼</li>
<li>push %rbp是幹嘛</li>
</ol>
<h4>釋疑</h4>
<p>(以下都是個人理解,諸君請自便)</p>
<ol>
<li>gcc使用at&t彙編 通用寄存器寄存器rbp rsp eax (64位)
<ul>
<li>rsp 相當於32位cpu中的 esp(Stack Pointer) 一般存放棧頂指針</li>
<li>rbp 相當於32位cpu中的 rbp(Base Pointer) 一般存放棧幀的基址</li>
<li>eax 累加寄存器</li>
</ul>
</li>
<li>at&t mov的語法是 mov 源地 目的,和intel彙編正好相反,movl的l是指定long。如果是mov,會根據前後操作數判斷要mov的長度。(不權威,具體細節請自行google)</li>
<li>$+數字 是取立即數</li>
<li>-0x8(%rbp)表示rbp中存放的地址再減8字節</li>
</ol>
<h4>棧的結構及函數調用</h4>
<p>要解釋以上幾個疑點就不得不研究一下棧結構及函數調用了</p>
<p>在上面的例子中,調用的函數是main,比較特殊,我們先把它當成一個普通被調用的函數。</p>
<p>我們看一下進程內存佈局</p>
<p><img src="http://s2.sinaimg.cn/middle/67e1c5cc4ae9ad71b3f11&690&690" alt="Mou icon" /></p>
<p>棧是由高地址向低地址增長</p>
<h6>棧幀</h6>
<p>棧幀就是rbp到rsp之間的內存塊,rbp的地址是高於rsp的</p>
<p>找一張32位的圖</p>
<p><img src="http://s10.sinaimg.cn/mw690/b5205e1dgx6CotFq37H09&690" alt="Mou icon" /></p>
<p>一個棧幀對應c代碼中的一個函數</p>
<pre><code>int func()
{
...code...
}
</code></pre>
<p>現在來看一下我們的main函數的棧佈局</p>
<p>最開始的時候main的棧裏是空的, 我們先執行<code>push %rbp</code>,於是棧變成了這樣</p>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td>rsp</td>
</tr>
</tbody>
</table>
<p>之後<code>mov %rsp,%rbp</code>,於是rbp由原來的棧底跳到了棧頂變成了新的棧底</p>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td>rsp rbp</td>
</tr>
</tbody>
</table>
<pre><code>movl $0x0,-0x8(%rbp)
</code></pre>
<p>通過這句話和源碼的對照</p>
<pre><code>int i = 0;
</code></pre>
<p>我們確定i處在rbp-8處</p>
<p>類似推理,a處在rbp-4處</p>
<p>於是棧應該是這樣的</p>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td>rbp rsp</td>
</tr>
<tr>
<td>int a</td>
<td>rbp-4</td>
</tr>
<tr>
<td>int i</td>
<td>rbp-8</td>
</tr>
</tbody>
</table>
<p>但是rsp沒變啊,爲什麼呢,因爲這個函數調用是葉子節點,沒必要把rsp移動了</p>
<p>這個分析的不太爽,我們再來一個</p>
<h2>另一個例子</h2>
<h4>代碼</h4>
<pre><code>int test(int a)
{
return 0;
}
int main()
{
int a = 1;
test(9);
return 0;
}
</code></pre>
<h4>彙編</h4>
<pre><code>00000000004005dc <_Z4testi>:
4005dc: 55 push %rbp
4005dd: 48 89 e5 mov %rsp,%rbp
4005e0: 89 7d fc mov %edi,-0x4(%rbp)
4005e3: b8 00 00 00 00 mov $0x0,%eax
4005e8: c9 leaveq
4005e9: c3 retq
00000000004005ea <main>:
4005ea: 55 push %rbp
4005eb: 48 89 e5 mov %rsp,%rbp
4005ee: 48 83 ec 10 sub $0x10,%rsp
4005f2: c7 45 fc 01 00 00 00 movl $0x1,-0x4(%rbp)
4005f9: bf 09 00 00 00 mov $0x9,%edi
4005fe: e8 d9 ff ff ff callq 4005dc <_Z4testi>
400603: b8 00 00 00 00 mov $0x0,%eax
400608: c9 leaveq
400609: c3 retq
</code></pre>
<p>因爲有函數調用,所以這是必須爲a分配空間</p>
<pre><code>int a = 0;
sub $0x10,%rsp
</code></pre>
<ul>
<li>最開始的時候main的棧裏是空的, 我們先執行<code>push %rbp</code>,於是棧變成了這樣</li>
</ul>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td>rsp</td>
</tr>
</tbody>
</table>
<ul>
<li>之後<code>mov %rsp,%rbp</code>,於是rbp由原來的棧底跳到了棧頂變成了新的棧底</li>
</ul>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td>rsp rbp</td>
</tr>
</tbody>
</table>
<ul>
<li>之後<code>sub $0x10,%rsp</code>,於是rsp下移16字節(有沒有感覺很奇怪,爲啥移動16字節呢,a只有4字節啊,哦,對了,64位cpu,這就是傳說中的字節對齊吧)</li>
</ul>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td>rbp</td>
</tr>
<tr>
<td>int a</td>
<td></td>
</tr>
<tr>
<td>12字節</td>
<td>rsp</td>
</tr>
</tbody>
</table>
<ul>
<li><code>movl $0x0,-0x4(%rbp)</code>,a被賦值爲0</li>
</ul>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td>rbp</td>
</tr>
<tr>
<td>int a</td>
<td></td>
</tr>
<tr>
<td>12字節</td>
<td>rsp</td>
</tr>
</tbody>
</table>
<ul>
<li><code>mov $0x9,%edi</code>, 把立即數9傳入寄存器edi,作爲test的調用參數</li>
</ul>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td>rbp</td>
</tr>
<tr>
<td>int a</td>
<td></td>
</tr>
<tr>
<td>12字節</td>
<td>rsp</td>
</tr>
</tbody>
</table>
<ul>
<li><code>callq 4005dc <_Z4testi></code>,把返回地址壓入棧頂,把指令指針rip跳轉到test的開頭處</li>
</ul>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td>rbp</td>
</tr>
<tr>
<td>int a</td>
<td></td>
</tr>
<tr>
<td>12字節</td>
<td></td>
</tr>
<tr>
<td>0x400603(mov $0x0,%eax)</td>
<td>rsp</td>
</tr>
</tbody>
</table>
<ul>
<li>test 中的<code>push %rbp</code></li>
</ul>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td></td>
</tr>
<tr>
<td>int a</td>
<td></td>
</tr>
<tr>
<td>12字節</td>
<td></td>
</tr>
<tr>
<td>0x400603(mov $0x0,%eax)</td>
<td>rsp</td>
</tr>
<tr>
<td>上一幀的rbp</td>
<td>rbp</td>
</tr>
</tbody>
</table>
<ul>
<li>test 中的<code>mov %rsp,%rbp</code></li>
</ul>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td></td>
</tr>
<tr>
<td>int a</td>
<td></td>
</tr>
<tr>
<td>12字節</td>
<td></td>
</tr>
<tr>
<td>0x400603(mov $0x0,%eax)</td>
<td></td>
</tr>
<tr>
<td>上一幀的rbp</td>
<td>rbp rsp</td>
</tr>
</tbody>
</table>
<p>假設test不是葉子節點,那麼rsp應該還得往下移動</p>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td></td>
</tr>
<tr>
<td>int a</td>
<td></td>
</tr>
<tr>
<td>12字節</td>
<td></td>
</tr>
<tr>
<td>0x400603(mov $0x0,%eax)</td>
<td></td>
</tr>
<tr>
<td>上一幀的rbp</td>
<td>rbp</td>
</tr>
<tr>
<td>data</td>
<td></td>
</tr>
<tr>
<td>data</td>
<td>rsp</td>
</tr>
</tbody>
</table>
<ul>
<li>我們直接跳到<code>leaveq</code> rsp跳轉到rbp 然後出棧值放入 rbp</li>
</ul>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td>rbp</td>
</tr>
<tr>
<td>int a</td>
<td></td>
</tr>
<tr>
<td>12字節</td>
<td></td>
</tr>
<tr>
<td>0x400603(mov $0x0,%eax)</td>
<td>rsp</td>
</tr>
</tbody>
</table>
<ul>
<li><code>retq</code> 出棧,將出棧的值 0x400603(mov $0x0,%eax) 賦值給 rip</li>
</ul>
<table border="1">
<thead>
<tr>
<th>內容</th>
<th>地址</th>
</tr>
</thead>
<tbody>
<tr>
<td>上一幀的rbp</td>
<td>rbp</td>
</tr>
<tr>
<td>int a</td>
<td></td>
</tr>
<tr>
<td>12字節</td>
<td></td>
</tr>
</tbody>
</table>
<p>這樣我們就恢復了在main中的棧,並且從0x400603(mov $0x0,%eax)繼續執行</p>
<h4>附:gdb調試過程</h4>
<pre><code>[email protected]:~/projects/test$ gdb a.out
GNU gdb (GDB) SUSE (7.0-0.4.16)
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-suse-linux".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /data/home/wangli/projects/test/a.out...done.
(gdb) display /i $pc
(gdb) b main
Breakpoint 1 at 0x4005ee
(gdb) r
Starting program: /data/home/wangli/projects/test/a.out
Failed to read a valid object file p_w_picpath from memory.
Missing separate debuginfo for /usr/lib64/libstdc++.so.6
Try: zypper install -C "debuginfo(build-id)=e907b88d15f5e1312d1ae0c7c61f8da92745738b"
Missing separate debuginfo for /lib64/libgcc_s.so.1
Try: zypper install -C "debuginfo(build-id)=3f06bcfc74f9b01780d68e89b8dce403bef9b2e3"
[Thread debugging using libthread_db enabled]
Breakpoint 1, 0x00000000004005ee in main ()
1: x/i $pc
0x4005ee <main+4>: sub $0x10,%rsp
(gdb) info registers rip rdi rsp rbp
rip 0x4005ee 0x4005ee <main+4>
rdi 0x1 1
rsp 0x7fffffffe370 0x7fffffffe370
rbp 0x7fffffffe370 0x7fffffffe370
(gdb) si
0x00000000004005f2 in main ()
1: x/i $pc
0x4005f2 <main+8>: movl $0x1,-0x4(%rbp)
(gdb) info registers rip rdi rsp rbp
rip 0x4005f2 0x4005f2 <main+8>
rdi 0x1 1
rsp 0x7fffffffe360 0x7fffffffe360
rbp 0x7fffffffe370 0x7fffffffe370
(gdb) si
0x00000000004005f9 in main ()
1: x/i $pc
0x4005f9 <main+15>: mov $0x9,%edi
(gdb) info registers rip rdi rsp rbp
rip 0x4005f9 0x4005f9 <main+15>
rdi 0x1 1
rsp 0x7fffffffe360 0x7fffffffe360
rbp 0x7fffffffe370 0x7fffffffe370
(gdb) si
0x00000000004005fe in main ()
1: x/i $pc
0x4005fe <main+20>: callq 0x4005dc <_Z4testi>
(gdb) info registers rip rdi rsp rbp
rip 0x4005fe 0x4005fe <main+20>
rdi 0x9 9
rsp 0x7fffffffe360 0x7fffffffe360
rbp 0x7fffffffe370 0x7fffffffe370
(gdb) si
0x00000000004005dc in test(int) ()
1: x/i $pc
0x4005dc <_Z4testi>: push %rbp
(gdb) info registers rip rdi rsp rbp
rip 0x4005dc 0x4005dc <test(int)>
rdi 0x9 9
rsp 0x7fffffffe358 0x7fffffffe358
rbp 0x7fffffffe370 0x7fffffffe370
(gdb) x/3xg 0x7fffffffe358
0x7fffffffe358: 0x0000000000400603 0x00007fffffffe440
0x7fffffffe368: 0x0000000100000000
(gdb) si
0x00000000004005dd in test(int) ()
1: x/i $pc
0x4005dd <_Z4testi+1>: mov %rsp,%rbp
(gdb) info registers rip rdi rsp rbp
rip 0x4005dd 0x4005dd <test(int)+1>
rdi 0x9 9
rsp 0x7fffffffe350 0x7fffffffe350
rbp 0x7fffffffe370 0x7fffffffe370
(gdb) x/3xg 0x7fffffffe350
0x7fffffffe350: 0x00007fffffffe370 0x0000000000400603
0x7fffffffe360: 0x00007fffffffe440
(gdb) si
0x00000000004005e0 in test(int) ()
1: x/i $pc
0x4005e0 <_Z4testi+4>: mov %edi,-0x4(%rbp)
(gdb) info registers rip rdi rsp rbp
rip 0x4005e0 0x4005e0 <test(int)+4>
rdi 0x9 9
rsp 0x7fffffffe350 0x7fffffffe350
rbp 0x7fffffffe350 0x7fffffffe350
(gdb) si
0x00000000004005e3 in test(int) ()
1: x/i $pc
0x4005e3 <_Z4testi+7>: mov $0x0,%eax
(gdb) si
0x00000000004005e8 in test(int) ()
1: x/i $pc
0x4005e8 <_Z4testi+12>: leaveq
(gdb) info registers rip rdi rsp rbp
rip 0x4005e8 0x4005e8 <test(int)+12>
rdi 0x9 9
rsp 0x7fffffffe350 0x7fffffffe350
rbp 0x7fffffffe350 0x7fffffffe350
(gdb) si
0x00000000004005e9 in test(int) ()
1: x/i $pc
0x4005e9 <_Z4testi+13>: retq
(gdb) info registers rip rdi rsp rbp
rip 0x4005e9 0x4005e9 <test(int)+13>
rdi 0x9 9
rsp 0x7fffffffe358 0x7fffffffe358
rbp 0x7fffffffe370 0x7fffffffe370
(gdb) si
0x0000000000400603 in main ()
1: x/i $pc
0x400603 <main+25>: mov $0x0,%eax
(gdb) info registers rip rdi rsp rbp
rip 0x400603 0x400603 <main+25>
rdi 0x9 9
rsp 0x7fffffffe360 0x7fffffffe360
rbp 0x7fffffffe370 0x7fffffffe370
(gdb)
</code></pre>