0%

汇编语法基础

很基本的汇编语法基础,在学习linux kernel的过程中经常会出现一些汇编代码,特在此记录一些基础用法,以便查阅。

本文主要总结了以下几个部分的知识:

  1. 为了理解汇编语句而需要了解的CPU寄存器;
  2. 简单汇编语句的语法规则;
  3. 函数调用在汇编层面的实现以及如何实现的函数调用堆栈;
  4. C语言中嵌入式汇编代码的基本语法

AT&T汇编和Intel汇编

汇编语言分为AT&T汇编和Intel汇编,Linux内核采用的汇编格式时AT&T汇编;

两种格式的对比:

1
2
3
4
5
# AT&T汇编:
movl %edx,%eax

# Intel汇编:
MOV EAX,EDX,

CPU的寄存器

16位CPU

以8086的16位CPU为例,寄存器分为:

  1. 通用寄存器
    1. 数据寄存器:AX、BX、CX、DX
    2. 指针寄存器:SP、BP
    3. 变址寄存器:SI、DI
  2. 控制寄存器
    1. 指令指针寄存器:IP
    2. 标志寄存器:Flags
  3. 段寄存器
    1. 代码段寄存器
    2. 数据段寄存器
    3. 堆栈段寄存器
    4. 附加段寄存器

32位CPU

32位CPU的寄存器的名称基本是以大写字母E开头;

64位CPU

64位CPU增加了8个通用寄存器(R8-R15),所以总共16个通用寄存器,这些通用寄存器的作用约定大致如下:

  1. RAX作为函数的返回值使用;
  2. RSP栈指针寄存器,指向栈顶;
  3. RDI、RSI、RDX、RCX、R8、R9用作函数参数;
  4. RBX、RBP、R10、R11、R12、R13、R14、R15用作数据存储

一般来说,64位CPU的寄存器名一般以大写字母R开头;

64位CPU向后兼容;

基本汇编的语法规则

最常见的指令——mov指令

1
2
3
4
5
6
7
8
9
10
# 把rax寄存器中的值移到rdx寄存器中
movq %rax,%rdx
# 8位
movb
# 16位
movw
# 32位
movl
# 64位
movq

汇编中的操作数

操作数分为立即数、寄存器、存储器三种:

1
2
3
4
5
# 立即数,$后面跟常数
$8
# 寄存器,%后面跟着寄存器的名字
%rax
# 存储器,根据计算出的有效地址来访问存储器的某个位置

寻址方式

寄存器寻址
1
movq %rax,%rdx
立即寻址
1
2
# 把0x123这个十六进制的数值直接放到RDX寄存器中
movq $0x123, %rdx
直接寻址

直接寻址(direct)是直接用一个数值,开头没有\(符号。开头有\)符号的数值表示这是一个立即数;没有$ 符号表示这是一个地址。例如:

1
2
# 把十六进制的0x123内存地址所指向的那块内存里存储的数据放到EDX寄存器里
movl 0x123, %edx
间接寻址

间接寻址( indirect)就是寄存器加个小括号。举例说明,%ebx这个寄存器中存的值是一个内存地 址,加个小括号表示这个内存地址所存储的数据,我们把它放到EDX寄存器中

1
movl (%ebx), %edx
变址寻址

在间接寻址的基础上,在原地址上面加上一个立即数

1
movl 4(%ebx), %edx

堆栈操作_push和pop

1
2
3
4
5
6
7
8
9
10
# 入栈
pushl %eax
# 等于以下两个命令结合
subl $4, %esp
movl %eax, (%esp)
# 出栈
popl %eax
# 等于以下两个命令结合
movl (%esp), %eax
addl $4, %esp

函数的调用和返回——call命令

1
2
3
4
5
6
7
8
9
# 从内存的0x12345处取指令执行
call 0x12345
# 等于以下两条指令
pushl %eip
movl $0x12345, %eip
# 返回
ret
# 等于以下两条指令
popl %eip

注意这里其实eip寄存器是不允许被程序修改的,所以下面的两条指令是不存在的;后面涉及到eip寄存器的指令同理;

函数调用堆栈的建立和撤销

为什么需要建立函数调用堆栈

堆栈是 C 语言程序运行时必须使用的记录函数调用路径和参数存储的空间,堆栈具体的作用有

  1. 记录函数调用框架(路径)
  2. 传递函数参数
  3. 保存返回值的地址
  4. 函数内部局部变量的存储空间
  5. ...

两个重要的寄存器

ESP:堆栈指针

EBP:基址指针

enter命令和leave命令

1
2
3
4
5
6
7
# enter 指令用来建立函数堆栈,等价于
pushl %ebp
movl %esp, %ebp

# leave 指令用来撤销函数堆栈,等价于下面两条指令
movl %ebp,%esp
popl %ebp

函数堆栈框架的形成

以一个简单的C函数汇编为例

我们来分析一个简单c函数的汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
// main.c
int g(int x)
{
return x + 3;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(8) + 1;
}
1
gcc -S -o main.s main.c -m32

C语言中的嵌入式汇编代码

内嵌汇编语法如下:

1
2
3
4
__asm__ __volatile__ ( 汇编语句模板:
输出部分:
输入部分:
破坏描述部分);
  1. __asm__是 GCC 关键字 asm 的宏定义,是内嵌汇编的关键字,表示这是一条内嵌汇编 语句。__asm__和 asm 可以互相替换使用:#define __asm__ asm

  2. __volatile__是 GCC 关键字 volatile 的宏定义,告诉编译器不要优化代码,汇编指令保留原样。__volatile__和 volatile 可以互相替换使用:#define __volatile__ volatile
  3. C内嵌汇编和普通汇编有一些不同,体现在%转义符号。寄存器前面会多一个%的转义符号, 有两个%;而%加一个数字则表示第二部分输出、第三部分输入以及第四部分破坏描述(没有破坏则可省略)的编号。不明白没关系,我们来看个例子:

用汇编代码实现 val3 = val1 + val2 的功能

第 1 行语句“movl $0,%%eax”是把 EAX 清 0。

第2行语句“addl %1,%%eax”,%1是指下面的输出和输入的部分,第一个输出编 号为%0,第二个编号为%1,第三个就是%2。%1 是指 val1,前面有一个“c”,是指用 ECX 寄存器存储 val1 的值,这样编译器在编译时就自动把 val1 的值放到 ECX 里面。%1 实际上 就是把 ECX 的值与 EAX 寄存器求和然后放到 EAX 寄存器中,本例中由于 EAX 为 0,所 以结果是 ECX 的值放入了 EAX 寄存器。

第 3 行语句“addl %2,%%eax”,%2 是指 val2 存在 EDX 寄存器中,就是把 val1 的 值加上 val2 的值再放到 EAX 里。

最后一条指令“movl %%eax,%0”是把val1加上val2的值存储的地方放到%0,%0 就是 val3,我们这里用=m 修饰,它的意思就是写到内存变量里面去,m 就是内存 memory, 不是使用寄存器了,这条指令是直接把变量放到内存 val3 里面。

至此,这段代码就实现了 val3 = val1 + val2 的功能。