linux下汇编语言开发总结

admin 2022年5月27日10:01:09评论42 views字数 4968阅读16分33秒阅读模式
汇编语言是直接对应系统指令集的低级语言,在语言越来越抽象的今天,汇编语言并不像高级语言那样使用广泛,仅仅在驱动程序,嵌入式系统等对性能要求苛刻的领域才能见到它们的身影。但是这并不表示汇编语言就已经没有用武之地了,通过阅读汇编代码,有助于我们理解编译器的优化能力,并分析代码中隐含的低效率,所以能够阅读和理解汇编代码也是一项很重要的技能。因为我平时都是在linux环境下工作的,这篇文章就讲讲linux下的汇编语言。
一、汇编语法风格
汇编语言分为intel风格和AT&T风格,前者被Microsoft Windows/Visual C++采用,Linux下,基本采用的是AT&T风格汇编,两者语法有很多不同的地方。
1. 寄存器访问格式不同。在 AT&T 汇编格式中,寄存器名要加上 '%' 作为前缀;而在 Intel 汇编格式中,寄存器名不需要加前缀。例如:
AT&T
Intel
pushl %eax
push eax
2. 立即数表示不同。在 AT&T 汇编格式中,用 '$' 前缀表示一个立即操作数;而在 Intel 汇编格式中,立即数的表示不用带任何前缀。例如:
AT&T
Intel
pushl $1
push 1
3. 操作数顺序不同。在 Intel 汇编格式中,目标操作数在源操作数的左边;而在 AT&T 汇编格式中,目标操作数在源操作数的右边。例如:
AT&T
Intel
addl $1, %eax
add eax, 1
4. 字长表示不同。在 AT&T 汇编格式中,操作数的字长由操作符的最后一个字母决定,后缀'b'、'w'、'l'分别表示操作数为byte、word和long;而在 Intel 汇编格式中,操作数的字长是用 "byte ptr" 和 "word ptr" 等前缀来表示的。例如:
AT&T
Intel
movb val, %eax
mov al, byte ptr val
5. 寻址方式表示不同。在 AT&T 汇编格式中,内存操作数的寻址方式是 
section:disp(base, index, scale)
而在 Intel 汇编格式中,内存操作数的寻址方式为:
section:[base + index*scale + disp]
由于 Linux 工作在保护模式下,用的是 32 位线性地址,所以在计算地址时不用考虑段基址和偏移量,而是采用如下的地址计算方法:
disp + base + index * scale
由此分为以下几种寻址方式:

Intel
AT&T
内存直接寻址
seg_reg: [base + index * scale + immed32]
seg_reg: immed32 (base, index, scale)
寄存器间接寻址
[reg]
(%reg)
寄存器变址寻址
[reg + _x]
_x(%reg)
立即数变址寻址
[reg + 1]
1(%reg)
整数数组寻址
[eax*4 + array]
_array (,%eax, 4)
二、IA32寄存器
1.通用寄存器
顾名思义,通用寄存器是那些你可以根据自己的意愿使用的寄存器,但有些也有特殊作用,IA32处理器包括8个通用寄存器,分为3组
1) 数据寄存器
EAX 累加寄存器,常用于运算;在乘除等指令中指定用来存放操作数,另外,所有的I/O指令都使用这一寄存器与外界设备传送数据。
EBX 基址寄存器,常用于地址索引
ECX 计数寄存器,常用于计数;常用于保存计算值,如在移位指令,循环(loop)和串处理指令中用作隐含的计数器.
EDX 数据寄存器,常用于数据传递。
2) 变址寄存器
ESI 源地址指针
EDI 目的地址指针
3) 指针寄存器
EBP为基址指针(Base Pointer)寄存器,存储当前栈帧的底部地址。
ESP为堆栈指针(Stack Pointer)寄存器,一直记录栈顶位置,不可直接访问,push时ESP减小,pop时增大。
2. 指令指针寄存器
EIP 保存了下一条要执行的指令的地址, 每执行完一条指令EIP都会增加当前指令长度的位移,指向下一条指令。用户不可直接修改EIP的值,但jmp、call和ret等指令也会改变EIP的值,jmp将EIP修改为目的指令地址,call修改EIP为被调函数第一条指令地址,ret从栈中取出(pop)返回地址存入EIP。
三、函数调用过程
函数调用时的具体步骤如下:
1. 调用函数将被调用函数参数入栈,入栈顺序由调用约定规定,包括cdecl,stdcall,fastcall,naked call等,c编译器默认使用cdecl约定,参数从右往座入栈。
2. 执行call命令。
call命令做了两件事情,一是将EIP寄存器内的值压入栈中,称为返回地址,函数完成后还要到这个地址继续执行程序。然后将被调用函数第一条指令地址存入EIP中,由此进入被调函数。
3. 被调函数开始执行,先准备当前栈帧的环境,分为3步
pushl %ebp 保存调用函数的基址到栈中,
movl %esp, %ebp 设置EBP为当前被调用函数的基址指针,即当前栈顶
subl $xx, %esp 为当前函数分配xx字节栈空间用于存储局部变量
4. 执行被调函数主体
5. 被调函数结束返回,恢复现场,第3步的逆操作,由leave和ret两条指令完成,
leave 主要恢复栈空间,相当于
movl %ebp, %esp 释放被调函数栈空间
popl %ebp 恢复ebp为调用函数基址
ret 与call指令对应,等于pop %EIP,
6. 返回到调用函数,从下一条语句继续执行
我们来看两个具体例子,第一个求数组和,
int ArraySum(int *array, int n){  int t = 0;  for(int i=0; i<n; ++i) t += array[i];  return t;}
int main() { int a[5] = {1, 2, 3, 4, 5 }; int sum = ArraySum(a, 5); return sum;}

编译成汇编代码
gcc -std=c99 -S -o sum.s sum.c
gcc加入了很多汇编器和连接器用到的指令,与我们讨论的内容无关,简化汇编代码如下:
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<<2, 即i*4, 一个int4字节    addl    8(%ebp), %eax  //得到array[i]地址,array+i*4    movl    (%eax), %eax   //array[i]    addl    %eax, -8(%ebp) //t+=array[i]    addl    $1, -4(%ebp).L2:    movl    -4(%ebp), %eax       cmpl    12(%ebp), %eax  //比较i<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[0] movl $2, -20(%ebp) //初始化a[1] movl $3, -16(%ebp) //初始化a[2] movl $4, -12(%ebp) //初始化a[3] movl $5, -8(%ebp) //初始化a[4] 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

栈变化过程如下:
linux下汇编语言开发总结linux下汇编语言开发总结
                         执行call指令前                                 执行call指令后
从图中可以看出
1. 数组连续排列,用move指令逐个赋值,读取数组元素方法是,用leal得到数组首地址,再计算偏移量
2. 参数从右往左入栈
3. gcc为了保证数据是严格对齐的,分配的空间大于使用的空间,有部分空间是浪费的
下面这个例子说明了struct结构的实现方法,
struct Point{  int x;  int y;};void PointInit(struct Point *p, int x, int y){  p->x = x;  p->y = y;}
int main() { struct Point p; int x = 10; int y = 20; PointInit(&p, x, y); return 0;}

编译成汇编代码,简化如下:
PointInit:    pushl    %ebp    movl    %esp, %ebp    movl    8(%ebp), %eax    //p的地址    movl    12(%ebp), %edx  //x    movl    %edx, (%eax)      //p->x=x    movl    8(%ebp), %eax    movl    16(%ebp), %edx  //y    movl    %edx, 4(%eax)    //p->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地址&p movl %eax, (%esp) call PointInit movl $0, %eax leave ret

栈图就不画了,可以清楚地看出struct跟数组类似,连续排列,通过相对位移访问struct的成员,p->y与*(p+sizeof(p->x))有一样的效果。
disassembleobjdump
在linux下有两个跟汇编有重要关系的命令,一个是objdump,另一个是gdb中的disassemble。
objdump帮助我们从可执行文件中反汇编出汇编代码,从而逆向分析工程。
objdump -d sum
部分汇编代码如下


080483b4 <ArraySum>: 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 <ArraySum+0x28> 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 <ArraySum+0x16> 80483e4: 8b 45 f8 mov -0x8(%ebp),%eax 80483e7: c9 leave  80483e8:    c3                       ret
disassemble可以显示调试程序的汇编代码,用法如下
disas 反汇编当前函数
disas sum 反汇编sum函数
disas 0x801234 反汇编位于地址 0x801234附近的函数
disas 0x801234 0x802234 返汇编指定范围内函数
 
 
reference:
http://zh.wikipedia.org/wiki/%E6%B1%87%E7%BC%96
http://www.ibm.com/developerworks/cn/linux/l-assembly/

作者:coderkian
出处:http://www.cnblogs.com/coderkian/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文始发于微信公众号(汇编语言):linux下汇编语言开发总结

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年5月27日10:01:09
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   linux下汇编语言开发总结http://cn-sec.com/archives/1055534.html

发表评论

匿名网友 填写信息