linux下汇编语言开发总结

汇编语言是直接对应系统指令集的低级语言,在语言越来越抽象的今天,汇编语言并不像高级语言那样使用广泛,仅仅在驱动程序,嵌入式系统等对性能要求苛刻的领域才能见到它们的身影。但是这并不表示汇编语言就已经没有用武之地了,通过阅读汇编代码,有助于我们理解编译器的优化能力,并分析代码中隐含的低效率,所以能够阅读和理解汇编代码也是一项很重要的技能。因为我平时都是在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, 一个int占4字节
    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

栈变化过程如下:

执行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/

linux下汇编语言开发总结

时间: 2024-08-06 00:43:29

linux下汇编语言开发总结的相关文章

【转载】Visual Studio 2015 for Linux更好地支持Linux下的开发

原文:Visual Studio 2015 for Linux更好地支持Linux下的开发 英文原文:Targeting Linux Made Easier in Visual Studio 2015 Visual C++ for Linux 扩展使 Visual Studio 2015 的用户可以在 VS2015 中编写C或者 C++ 代码,并将代码部署到基于 Linux 的系统中去编译和调试.源代码和项目文件通过 SSH 传输到远程机上,程序的输出将显示在 Visual Studio 上.

linux下C++开发工具

就C++开发工具而言,与Windows下微软(VC, VS2005等)一统天下相比,Linux/Unix下C++开发,可谓五花八门,各式各样.Emacs, vi, eclipse, anjuta,kdevelop等层出不穷.Windows下,开发工具多以集成开发环境IDE的形式展现给最终用户.例如,VS2005集成了编辑器,宏汇编ml,C /C++编译器cl,资源编译器rc,调试器,文档生成工具, nmake.它们以集成方式提供给最终用户,对于初学者而言十分方便.但是,这种商业模式,直接导致用户

Linux下服务器端开发流程及相关工具介绍(C++)

原文:Linux下服务器端开发流程及相关工具介绍(C++) 去年刚毕业来公司后,做为新人,发现很多东西都没有文档,各种工具和地址都是口口相传的,而且很多时候都是不知道有哪些工具可以使用,所以当时就想把自己接触到的这些东西记录下来,为后来者提供参考,相当于一个路线图,帮助新人尽快上手. 本文介绍的是阿里妈妈搜索直通车这边的一些开发流程及相关工具.做为新人入门手册,其中某些工具可能只有直通车这边在用,但对于其他公司的新人来说,一样是具有指导意义的. 简单介绍一下直通车这边的开发背景:直通车业务对外是

构建Linux下IMX257 开发环境

构建Linux下IMX257 开发环境 2015-03-09 李海沿 一直以来玩开发IMX257都是使用Windows,然后就必须开一个超级卡的虚拟机,接着就是使用securecrt远程登录虚拟机中的linux和通过串口登录开发板的linux系统. 这里,我们来实现将所有的开发环境工作都转移至linux操作系统下,这下我们就不用饱受虚拟机导致的电脑卡的痛苦了. 接下来,我们不如正题: 在linux下,我们可以有两种方法来实现:minicom 和 C-kerimit两种方法. 我刚开始使用的是mi

Linux下c开发 之 线程通信(转)

Linux下c开发 之 线程通信(转) 1.Linux“线程” 进程与线程之间是有区别的,不过Linux内核只提供了轻量进程的支持,未实现线程模型.Linux是一种“多进程单线程”的操作系统.Linux本身只有进程的概念,而其所谓的“线程”本质上在内核里仍然是进程. 大家知道,进程是资源分配的单位,同一进程中的多个线程共享该进程的资源(如作为共享内存的全局变量).Linux中所谓的“线程”只是在被创建时clone了父进程的资源,因此clone出来的进程表现为“线程”,这一点一定要弄清楚.因此,L

linux下c++开发环境安装(eclipse+cdt)

方法一: 此外,众所周知,Eclipse是Java程序,因此很容易就实现了跨平台,也是众所周知,Java的大型程序非常吃内存,即使有512MB内存, 仍然感觉Eclipse的启动速度很慢.个人认为1GB内存是你工作效率的保证.其余的东西,需待我深入学习以后再进一步分析. 据网友的经验,先安装Eclipse Classic,然后再装CDT(C/C++ Development Tool),这样虽然体积大了一点但是方便日后添加Tool Kit. 在Ubuntu上安装Eclipse有两种方法:1. 使用

(转)Linux下C++开发初探

1.开发工具 Windows下,开发工具多以集成开发环境IDE的形式展现给最终用户.例如,VS2008集成了编辑器,宏汇编ml,C /C++编译器cl,资源编译器rc,调试器,文档生成工具, nmake.它们以集成方式提供给最终用户,对于初学者而言十分方便.但是,这种商业模式,直接导致用户可定制性差,不利于自动化,集成第三方工具的能力弱.例如,无法定制一些宏来处理一些重复操作:体会不到自动化makefile一步到位快感:无法远程登录到服务器上进行开发:无法使用某种”粘合剂”来把第三方工具(例如,

Linux下golang开发环境搭建

对于golang开发来说,Windows下可以用vscode或者liteide都不错,但是Linux下的开发也就只有vim了,所以怎么搞笑的利用vim进行golang开发呢? 参考官方推荐的一个插件:vim-go 安装步骤: vim-go的安装需要使用vim插件管理工具,我使用的是VundleVim,具体的安装操作按照该工具的readme来操作即可. 当vim-go安装完成之后,按照vim-go的readme里面的介绍,需要用到命令:GoInstallBinaries来安装需要用的工具,但是这里

linux下lua开发环境安装

我前面我们介绍了nginx+lua环境的搭建,在此我们再来了解下lua开发环境的安装. 目前lua版本已经更新到lua5.3,但是我们在此安装lua5.1,因为5.1运行了好多年,有好多模块都是基于此版本的,例如lua_gd. 安装: 1.先安装lua的相关依赖 yum install readline-dev readline-devel 2.安装lua5.1 wget http://www.lua.org/ftp/lua-5.1.0.tar.gz tar -zxvf lua-5.1.0.ta