咸鱼情感 發表於 2020-10-26 14:17:00

汇编语言开发总结

<p>汇编语言是直接对应系统指令集的低级语言,在语言越来越抽象的今天,汇编语言并不像高级语言那样使用广泛,仅仅在驱动程序,嵌入式系统等对性能要求苛刻的领域才能见到它们的身影。但是这并不表示汇编语言就已经没有用武之地了,通过阅读汇编代码,有助于我们理解编译器的优化能力,并分析代码中隐含的低效率,所以能够阅读和理解汇编代码也是一项很重要的技能。因为我平时都是在linux环境下工作的,这篇文章就讲讲linux下的汇编语言。</p>
<p><strong>一、</strong><strong>汇编语法风格</strong></p>
<p>汇编语言分为intel风格和AT&T风格,前者被Microsoft Windows/Visual C++采用,Linux下,基本采用的是AT&T风格汇编,两者语法有很多不同的地方。</p>
<div>1. 寄存器访问格式不同。在 AT&amp;T 汇编格式中,寄存器名要加上 '%' 作为前缀;而在 Intel 汇编格式中,寄存器名不需要加前缀。例如:</div>
<div align="center">
<table border="1" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr>
<td width="110">
<p align="center">AT&amp;T</p>
</td>
<td width="205">
<p>Intel</p>
</td>
</tr>
<tr>
<td width="110">
<p>pushl %eax</p>
</td>
<td width="205">
<p>push eax</p>
</td>
</tr>
</tbody>
</table>
</div>
<div>2. 立即数表示不同。在 AT&amp;T 汇编格式中,用 '$' 前缀表示一个立即操作数;而在 Intel 汇编格式中,立即数的表示不用带任何前缀。例如:</div>
<div align="center">
<table border="1" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr>
<td width="111">
<p>AT&amp;T</p>
</td>
<td width="205">
<p>Intel</p>
</td>
</tr>
<tr>
<td width="111">
<p>pushl $1</p>
</td>
<td width="205">
<p>push 1</p>
</td>
</tr>
</tbody>
</table>
</div>
<div>3. 操作数顺序不同。在 Intel 汇编格式中,目标操作数在源操作数的左边;而在 AT&amp;T 汇编格式中,目标操作数在源操作数的右边。例如:</div>
<div align="center">
<table border="1" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr>
<td width="160">
<p>AT&amp;T</p>
</td>
<td width="156">
<p>Intel</p>
</td>
</tr>
<tr>
<td width="160">
<p>addl $1, %eax</p>
</td>
<td width="156">
<p>add eax, 1</p>
</td>
</tr>
</tbody>
</table>
</div>
<p>4. 字长表示不同。在 AT&amp;T 汇编格式中,操作数的字长由操作符的最后一个字母决定,后缀'b'、'w'、'l'分别表示操作数为byte、word和long;而在 Intel 汇编格式中,操作数的字长是用 "byte ptr" 和 "word ptr" 等前缀来表示的。例如:</p>
<table border="1" cellspacing="0" cellpadding="0">
<thead>
<tr>
<td width="160">
<p class="a" align="center">AT&amp;T</p>
</td>
<td width="198">
<p class="a" align="center">Intel</p>
</td>
</tr>
</thead>
<tbody>
<tr>
<td width="160">
<p class="a" align="center">movb val, %eax</p>
</td>
<td width="198">
<p class="a" align="center">mov al, byte ptr val</p>
</td>
</tr>
</tbody>
</table>
<p>5. 寻址方式表示不同。在 AT&amp;T 汇编格式中,内存操作数的寻址方式是&nbsp;</p>
<p>section:disp(base, index, scale)</p>
<p>而在 Intel 汇编格式中,内存操作数的寻址方式为:</p>
<p>section:</p>
<p>由于 Linux 工作在保护模式下,用的是 32 位线性地址,所以在计算地址时不用考虑段基址和偏移量,而是采用如下的地址计算方法:</p>
<p>disp + base + index * scale</p>
<div>由此分为以下几种寻址方式:</div>
<div align="center">
<table border="1" cellspacing="0" cellpadding="0" align="center">
<tbody>
<tr>
<td width="107">&nbsp;</td>
<td width="278">
<p>Intel</p>
</td>
<td width="258">
<p>AT&T</p>
</td>
</tr>
<tr>
<td width="107">
<p>内存直接寻址</p>
</td>
<td width="278">
<p align="center">seg_reg: </p>
</td>
<td width="258">
<p>seg_reg: immed32 (base, index, scale)</p>
</td>
</tr>
<tr>
<td width="107">
<p>寄存器间接寻址</p>
</td>
<td width="278">
<p></p>
</td>
<td width="258">
<p>(%reg)</p>
</td>
</tr>
<tr>
<td width="107">
<p>寄存器变址寻址</p>
</td>
<td width="278">
<p></p>
</td>
<td width="258">
<p>_x(%reg)</p>
</td>
</tr>
<tr>
<td width="107">
<p>立即数变址寻址</p>
</td>
<td width="278">
<p></p>
</td>
<td width="258">
<p>1(%reg)</p>
</td>
</tr>
<tr>
<td width="107">
<p>整数数组寻址</p>
</td>
<td width="278">
<p></p>
</td>
<td width="258">
<p>_array (,%eax, 4)</p>
</td>
</tr>
</tbody>
</table>
</div>
<p><strong>二、</strong><strong>IA32</strong><strong>寄存器</strong></p>
<p><strong>1.通用寄存器</strong></p>
<p>顾名思义,通用寄存器是那些你可以根据自己的意愿使用的寄存器,但有些也有特殊作用,IA32处理器包括8个通用寄存器,分为3组</p>
<p>1) 数据寄存器</p>
<p>EAX 累加寄存器,常用于运算;在乘除等指令中指定用来存放操作数,另外,所有的I/O指令都使用这一寄存器与外界设备传送数据。</p>
<p>EBX 基址寄存器,常用于地址索引</p>
<p>ECX 计数寄存器,常用于计数;常用于保存计算值,如在移位指令,循环(loop)和串处理指令中用作隐含的计数器.<br>EDX 数据寄存器,常用于数据传递。</p>
<p>2) 变址寄存器</p>
<p>ESI 源地址指针</p>
<p>EDI 目的地址指针</p>
<p>3) 指针寄存器</p>
<p>EBP为基址指针(Base Pointer)寄存器,存储当前栈帧的底部地址。</p>
<p>ESP为堆栈指针(Stack Pointer)寄存器,一直记录栈顶位置,不可直接访问,push时ESP减小,pop时增大。</p>
<p><strong>2. 指令指针寄存器</strong></p>
<p>EIP 保存了下一条要执行的指令的地址, 每执行完一条指令EIP都会增加当前指令长度的位移,指向下一条指令。用户不可直接修改EIP的值,但jmp、call和ret等指令也会改变EIP的值,jmp将EIP修改为目的指令地址,call修改EIP为被调函数第一条指令地址,ret从栈中取出(pop)返回地址存入EIP。</p>
<p><strong>三、</strong><strong>函数调用过程</strong></p>
<p>函数调用时的具体步骤如下:</p>
<p>1. 调用函数将被调用函数参数入栈,入栈顺序由调用约定规定,包括cdecl,stdcall,fastcall,naked call等,c编译器默认使用cdecl约定,参数从右往座入栈。</p>
<p>2. 执行call命令。</p>
<p>call命令做了两件事情,一是将EIP寄存器内的值压入栈中,称为返回地址,函数完成后还要到这个地址继续执行程序。然后将被调用函数第一条指令地址存入EIP中,由此进入被调函数。</p>
<p>3. 被调函数开始执行,先准备当前栈帧的环境,分为3步</p>
<blockquote>
<p>pushl %ebp 保存调用函数的基址到栈中,</p>

</blockquote>
<blockquote>
<p>movl %esp, %ebp 设置EBP为当前被调用函数的基址指针,即当前栈顶</p>

</blockquote>
<blockquote>
<p>subl $xx, %esp 为当前函数分配xx字节栈空间用于存储局部变量</p>

</blockquote>
<p>4. 执行被调函数主体</p>
<p>5. 被调函数结束返回,恢复现场,第3步的逆操作,由leave和ret两条指令完成,</p>
<p>leave 主要恢复栈空间,相当于</p>
<blockquote>
<p>movl %ebp, %esp 释放被调函数栈空间</p>

</blockquote>
<blockquote>
<p>popl %ebp 恢复ebp为调用函数基址</p>

</blockquote>
<p>ret 与call指令对应,等于pop %EIP,</p>
<p>6. 返回到调用函数,从下一条语句继续执行</p>
<p>我们来看两个具体例子,第一个求数组和,</p>
<div class="cnblogs_Highlighter">
<pre class="brush:cpp;gutter:true;">int ArraySum(int *array, int n){
int t = 0;
for(int i=0; i&lt;n; ++i) t += array;
return t;
}

int main() {
int a = {1, 2, 3, 4, 5 };
int sum = ArraySum(a, 5);
return sum;
}
</pre>
</div>
<p>  </p>
<p>编译成汇编代码</p>
<p>gcc -std=c99 -S -o sum.s sum.c</p>
<p>gcc加入了很多汇编器和连接器用到的指令,与我们讨论的内容无关,简化汇编代码如下:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:as3;gutter:true;">ArraySum:
    pushl    %ebp
    movl    %esp, %ebp   
    subl    $16, %esp//分配16字节栈空间
    movl    $0, -8(%ebp)//初始化t
    movl    $0, -4(%ebp)//初始化i
    jmp    .L2
.L3:
    movl    -4(%ebp), %eax
    sall    $2, %eax//i&lt;&lt;2, 即i*4, 一个int占4字节
    addl    8(%ebp), %eax//得到array地址,array+i*4
    movl    (%eax), %eax   //array
    addl    %eax, -8(%ebp) //t+=array
    addl    $1, -4(%ebp)
.L2:
    movl    -4(%ebp), %eax   
    cmpl    12(%ebp), %eax//比较i&lt;n
    jl    .L3
    movl    -8(%ebp), %eax //return t; 默认eax存函数返回值
    leave
    ret

main:
.LFB1:
    pushl    %ebp
    movl    %esp, %ebp
    subl    $40, %esp      
    movl    $1, -24(%ebp) //初始化a
    movl    $2, -20(%ebp) //初始化a
    movl    $3, -16(%ebp) //初始化a
    movl    $4, -12(%ebp) //初始化a
    movl    $5, -8(%ebp)   //初始化a
    movl    $5, 4(%esp)    //5作为第二个参数传给 ArraySum
    leal    -24(%ebp), %eax//leal产生数组a的地址
    movl    %eax, (%esp)   //作为第一个参数传给ArraySum
    call    ArraySum
    movl    %eax, -4(%ebp)//返回值传给sum
    movl    -4(%ebp), %eax//return sum
    leave
    ret
</pre>
</div>
<p>  </p>
<p>栈变化过程如下:</p>
<p><img src="https://images0.cnblogs.com/i/569997/201407/122335156142000.png" alt="" width="265" height="342"><img src="https://images0.cnblogs.com/i/569997/201407/122335263325191.png" alt="" width="259" height="506"></p>
<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;执行call指令前 &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; 执行call指令后</p>
<p>从图中可以看出</p>
<p>1. 数组连续排列,用move指令逐个赋值,读取数组元素方法是,用leal得到数组首地址,再计算偏移量</p>
<p>2. 参数从右往左入栈</p>
<p>3. gcc为了保证数据是严格对齐的,分配的空间大于使用的空间,有部分空间是浪费的</p>
<p>下面这个例子说明了struct结构的实现方法,</p>
<div class="cnblogs_Highlighter">
<pre class="brush:cpp;gutter:true;">struct Point{
int x;
int y;
};
void PointInit(struct Point *p, int x, int y){
p-&gt;x = x;
p-&gt;y = y;
}

int main() {
struct Point p;
int x = 10;
int y = 20;
PointInit(&amp;p, x, y);
return 0;
}
</pre>
</div>
<p>  编译成汇编代码,简化如下:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:as3;gutter:true;">PointInit:
    pushl    %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %eax    //p的地址
    movl    12(%ebp), %edx//x
    movl    %edx, (%eax)      //p-&gt;x=x
    movl    8(%ebp), %eax
    movl    16(%ebp), %edx//y
    movl    %edx, 4(%eax)    //p-&gt;y=y
    popl    %ebp
    ret

main:
    pushl    %ebp
    movl    %esp, %ebp
    subl    $28, %esp
    movl    $10, -8(%ebp)//x=10
    movl    $20, -4(%ebp)y=20
    movl    -4(%ebp), %eax
    movl    %eax, 8(%esp)
    movl    -8(%ebp), %eax
    movl    %eax, 4(%esp)
    leal    -16(%ebp), %eax//取p地址&amp;p
    movl    %eax, (%esp)
    call    PointInit
    movl    $0, %eax
    leave
    ret
</pre>
</div>
<p>  </p>
<p>栈图就不画了,可以清楚地看出struct跟数组类似,连续排列,通过相对位移访问struct的成员,p-&gt;y与*(p+sizeof(p-&gt;x))有一样的效果。</p>
<p><strong>四</strong><strong>、</strong><strong>disassemble</strong><strong>和</strong><strong>objdump</strong></p>
<p>在linux下有两个跟汇编有重要关系的命令,一个是objdump,另一个是gdb中的disassemble。</p>
<p>objdump帮助我们从可执行文件中反汇编出汇编代码,从而逆向分析工程。</p>
<p>objdump -d sum</p>
<p>部分汇编代码如下</p>
<div class="cnblogs_Highlighter">
<pre class="brush:as3;gutter:true;">080483b4 &lt;ArraySum&gt;:
80483b4:    55                     push   %ebp
80483b5:    89 e5                  mov    %esp,%ebp
80483b7:    83 ec 10               sub    $0x10,%esp
80483ba:    c7 45 f8 00 00 00 00   movl   $0x0,-0x8(%ebp)
80483c1:    c7 45 fc 00 00 00 00   movl   $0x0,-0x4(%ebp)
80483c8:    eb 12                  jmp    80483dc &lt;ArraySum+0x28&gt;
80483ca:    8b 45 fc               mov    -0x4(%ebp),%eax
80483cd:    c1 e0 02               shl    $0x2,%eax
80483d0:    03 45 08               add    0x8(%ebp),%eax
80483d3:    8b 00                  mov    (%eax),%eax
80483d5:    01 45 f8               add    %eax,-0x8(%ebp)
80483d8:    83 45 fc 01            addl   $0x1,-0x4(%ebp)
80483dc:    8b 45 fc               mov    -0x4(%ebp),%eax
80483df:    3b 45 0c               cmp    0xc(%ebp),%eax
80483e2:    7c e6                  jl   80483ca &lt;ArraySum+0x16&gt;
80483e4:    8b 45 f8               mov    -0x8(%ebp),%eax
80483e7:    c9                     leave
80483e8:    c3                     ret
</pre>
</div>
<p>  </p>
<p>disassemble可以显示调试程序的汇编代码,用法如下</p>
<p>disas 反汇编当前函数</p>
<p>disas sum 反汇编sum函数</p>
<p>disas 0x801234 反汇编位于地址 0x801234附近的函数</p>
<p>disas 0x801234 0x802234 返汇编指定范围内函数</p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/navysummer/p/13878257.html
頁: [1]
查看完整版本: 汇编语言开发总结