linux汇编基础

文章目录

    • 简介
    • linux汇编格式
    • 常用寄存器
    • 常用指令及寻址方式

简介

大多数情况下 Linux 程序员不需要使用汇编语言,因为即便是硬件驱动这样的底层程序在 Linux 操作系统中也可以用完全用 C 语言来实现,再加上 GCC 这一优秀的编译器目前已经能够对最终生成的代码进行很好的优化,的确有足够的理由让我们可以暂时将汇编语言抛在一边了。但实现情况是 Linux 程序员有时还是需要使用汇编,或者不得不使用汇编,理由很简单:精简、高效和 libc 无关性。假设要移植 Linux 到某一特定的嵌入式硬件环境下,首先必然面临如何减少系统大小、提高执行效率等问题,此时或许只有汇编语言能帮上忙了。

汇编语言直接同计算机的底层软件甚至硬件进行交互,它具有如下一些优点:
• 能够直接访问与硬件相关的存储器或 I/O 端口;
• 能够不受编译器的限制,对生成的二进制代码进行完全的控制;
• 能够对关键代码进行更准确的控制,避免因线程共同访问或者硬件设备共享引起的死锁;
• 能够根据特定的应用对代码做最佳的优化,提高运行速度;
• 能够最大限度地发挥硬件的功能。

linux汇编格式

绝大多数 Linux 程序员以前只接触过DOS/Windows 下的汇编语言,这些汇编代码都是 Intel 风格的。但在 Unix 和 Linux 系统中,更多采用的还是 AT&T 格式,两者在语法格式上有着很大的不同。

  1. 在 AT&T 汇编格式中,寄存器名要加上 ‘%’ 作为前缀;而在 Intel 汇编格式中,寄存器名不需要加前缀。例如:
AT&T 格式 Intel 格式
pushl %eax push eax
  1. 在 AT&T 汇编格式中,用 ‘$’ 前缀表示一个立即操作数;而在 Intel 汇编格式中,立即数的表示不用带任何前缀。例如:
AT&T 格式 Intel 格式
pushl $1 pushl 1
  1. AT&T 和 Intel 格式中的源操作数和目标操作数的位置正好相反。在 Intel 汇编格式中,目标操作数在源操作数的左边;而在 AT&T 汇编格式中,目标操作数在源操作数的右边。例如:
    |AT&T 格式| Intel 格式 |
    |–|--|
    |addl $1, %eax |addl $1, %eax |
  2. 在 AT&T 汇编格式中,操作数的字长由操作符的最后一个字母决定,后缀’b’、‘w’、'l’分别表示操作数为字节(byte,8 比特)、字(word,16 比特)和长字(long,32比特);而在 Intel 汇编格式中,操作数的字长是用 “byte ptr” 和 “word ptr” 等前缀来表示的。例如:
AT&T 格式 Intel 格式
movb val, %al mov al, byte ptr val
  1. 在 AT&T 汇编格式中,绝对转移和调用指令(jump/call)的操作数前要加上’*'作为前缀,而在 Intel 格式中则不需要。 远程转移指令和远程子调用指令的操作码,在 AT&T 汇编格式中为 “ljump” 和 “lcall”,而在 Intel 汇编格式中则为 “jmp far” 和 “call far”,即:
AT&T 格式 Intel 格式
ljump $section, $offset ljump $section, $offsetl
  1. 在 AT&T 汇编格式中,内存操作数的寻址方式是
    section:disp(base, index, scale)
    而在 Intel 汇编格式中,内存操作数的寻址方式为:
    section:[base + index*scale + disp]

  2. 实际例子

AT&T 格式 Intel 格式
movl -4(%ebp), %eax mov eax, [ebp - 4]
movl array(, %eax, 4), %eax mov eax, [eax*4 + array]
movb $4, %fs:(%eax) mov fs:eax, 4
  1. 可能很多人都喜欢windows的intel格式,在linux下也可以对att格式做转换:
    set disassembly-flavor intel 转换为intel格式的汇编
    set disassembly-flavor att 转换为att格式的汇编

常用寄存器

这里主要讲解32bit的寄存器

  1. 通用寄存器
8位 16位 32位 描述
AH AL AX EAX 累加器
BH BL BX EBX 基地址寄存器
CH CL CX ECX 计数寄存器
DH DL DX EDX 数据寄存器
BP EBP 堆栈基指针
SI DI ESI EDI 变址寄存器
SP ESP 堆栈栈顶寄存器
  1. 段寄存器
    代码段寄存器CS:存放当前执行的程序的段地址。
    数据段寄存器DS:存放当前执行的程序操作数的段地址。
    堆栈段寄存器SS:存放当前执行的程序所用堆栈的段地址。
    附加段寄存器ES:存放当前程序中一个辅助数据段的段地址
  2. 专用寄存器
    指令寄存器:16位:IP,32位:EIP
    用来存放将要执行的下一条指令地址的偏移量,它与段寄存器CS联合形成代码段中指令的物理地址
    标志寄存器:这是存放条件码标志、控制标志的16位寄存器

常用指令及寻址方式

  1. 寻址方式
指令 含义 寻址方式
movl %eax,%edx edx = eax register mode
movl $0x123,%edx edx = 0x123 immediate
movl 0x123,%edx edx = (int32_t)0x123 direct
movl (%ebx),%edx edx = (int32_t)ebx indirect
movl 4(%ebx),%edx edx = (int32_t)(ebx + 4) displaced
  1. 几个重要的汇编指令:
    pushl %eax:压栈(注意栈是向下增长的)
    相当于: subl $4,%esp
    movl %eax,(%esp)

popl %eax:出栈并将值赋给eax寄存器
相当于:movl (%esp),%eax
addl $4,%esp

call 0x12345:跳转指令,常用在调用函数中,用来保存当前地址及跳转
相当于: pushl %eip() ,movl $0x12345,%eip()
//打(**)表示程序员不能直接对eip寄存器操作

ret:返回指令,用在函数返回时获取返回地址
相当于: popl %eip(*)

leave:恢复堆栈指令,用在函数返回时用来恢复调用方的堆栈信息。
相当于:movl %ebp, %esp
popl %ebp
要想理解这一点得先明白,在进入一个新的函数时,一般都会执行下面两条指令:
pushl %ebp,movl %esp,%ebp
这两句的功能就是保存原来的堆栈信息,然后开辟新的堆栈。先将堆栈原先的基址(ebp)入栈,以保存之前任务的信息。然后将栈顶指针的值赋给ebp,将之前的栈顶作为新的基址(栈底),然后再这个基址上开辟相应的空间用作被调用函数的堆栈。
那么相应的leave指令就比较好理解了,函数返回后,从ebp中可取出之前的esp,使栈顶恢复函数调用前的位置,再从回复后的栈顶弹出之前的ebp值。这样ebp和esp就都恢复了调用前的位置,堆栈恢复函数调用前的状态。

  1. 其它指令
    可参考:https://blog.csdn.net/ldong2007/article/details/2873611

下一篇我将用汇编来详细分析变量初始化,参数传递,函数调用,函数返回的原理。

你可能感兴趣的:(linux)