第06章下 内联汇编

GCC支持在c代码中直接嵌入汇编代码称为GCC inline assembly。c语言不支持寄存器操作,汇编语言可以,所以c语言中嵌入内联汇编提升操控力。

内联汇编按格式分为两大类:基本内联汇编,扩展内联汇编。

汇编语言有Intel语法和AT&T语法。linux下是AT&T语法。

Intel语法和AT&T语法的区别在于:

区别 Intel AT&T 说明
寄存器 寄存器前无前缀 寄存器前有前缀%
操作数顺序 目的数在左,源操作数在右 源操作数在左,目的操作数在右 Intel使用一种 目的操作数=源操作作数的形式。AT&T则是将源操作数送到目的操作数的形式
操作数指定大小 有关内存的操作数前加上类型锈蚀,8位byte,16为word,32位dword。使用方括号 指令的最后一个字母表示操作数大小,8b,16w,32l。不需要加方括号 AT&T中,内存地址是第一位,所以默认数字就是内存地址,操作数不加方括号。立即数前加$前缀
立即数 无前缀,默认数字就是立即数 立即数前面加上$,数字默认是内存地址

Intel语法中,立即数是普通数字,如果要表示内存地址,需要加上方括号。

而AT&T中,普通数字表示内存地址,如果要表示立即数需要使用$前缀修饰

AT&T内存寻址的格式为:base(offset,index,size),等价于base+offset+index*size.缺省数字是0。可以使用 movl %eax,(,%esi,2)

1 基本内联汇编

基本内联汇编格式为:asm [volatile] ("assembly code")

关键字之间可以使用空格或是制表符分割,也可以紧挨在一起不分割。

asm关键字用于声明内联汇编表达式,是内联汇编固定的部分。asm__asm__一样

因为gcc有个优化选项-O,可以指定优化级别。加上volatile关键字可以高速编译器,不要修改我的汇编代码,该关键字是可选的。

assembly code是汇编代码。必须在在圆括号内,使用双引号引起来:

  1. 指令必须用双引号包裹
  2. 一对双引号不能跨行,如果跨行,需要在结尾使用\转义
  3. 指令之间使用;分隔。
  4. 可以在一个圆括号里面,使用多个分好包括汇编语句,两个分好对之间不用分隔,直接紧靠

例如:

char *str="hello world \n";
int count=0;
void main()
{
    asm ("
        pusha;\           
        movl $4,%eax;\
        movl $1,%ebx;\
        movl str,%ecx;\
        movl $12,%edx;\
        int $0x80;\
        mov %eax,count;  //eax 中保存的是返回值
        popa\
    ");
}

基本内联汇编的缺点在于:

  1. 使用了具体的寄存器,如果要确保不影响其他部分的使用,需要先保存所有寄存器,最后再恢复。否则会造成寄存器的冲突

2 扩展内联汇编

扩展内联汇编解决了基本内联汇编的问题。

2.1 格式

扩展内联汇编的格式为:

asm [volatile] ("assembly code":output:input:clobber/modify)
// 只有最后一个可以省略

assembly code部分还是汇编代码,但是可以不再使用具体的寄存器等。

output:input这两部分用来指定汇编代码的数据如何输出给c代码使用。内嵌的汇编指令运行结束后,如果将运行结果存储在c变量中,就使用这两项。这两部分的格式为:"操作数修饰符约束名"(c变量名)

input用来指定c中数据如何输入给汇编使用。input中每个操作数的格式为:"[操作数修饰符] 约束名"(c变量名)引号和圆括号不能少。多个操作数之间使用,分隔

clobber/modify部分表示,在汇编代码执行后会破坏一些寄存器或内存资源,通过该项通知编译器,可能造成寄存器或数据的破坏,这样gcc就能够知道那些寄存器或是内存需要提前保存起来。如果汇编代码中修改了eflags寄存器,那么在该部分使用cc声明,如果output部分修改了内存,那么需要memory告诉gcc。gcc为了提速编译中又是会把内存中的数据缓存到寄存器中,之后处理都是直接从寄存器中去读取,而编译过程中无法检测内存的变化,只有程序在运行中能知道变化。因此,当程序执行的时候,内存已经改变,但是寄存器中的值还没改变,那么运行结果就是错的。因此volatile变量就是高速该变量容易改变,不要将其缓存到寄存器中。

2.2 约束

约束是用来修饰输入输出,或是告诉gcc编译器,对变量采用什么方式处理

2.2.1 寄存器约束:

寄存器约束指的是要求gcc使用那些寄存器,将input和output中变量约束在某个寄存器中,常见的寄存器约束有:

  1. abcd分别表示eax/ax/al,这16组寄存器
  2. DS表示edi/di和esi/si寄存器
  3. q表示任意4个通用寄存器,32位
  4. r表示任意6个寄存器,32为
  5. g表示,可以放在任意地点,寄存器或是内存
  6. A表示eax和edx组成64位整数
  7. f表示浮点寄存器
  8. tu表示第一个和第二个浮点寄存器
void main()
{
    int in_a=1,in_b=2,out_sum;
    asm("addl %%ebx,%%eax":"=a"(out_sum):"a"(in_a),"b"(in_b));
    // a之前的那个=号是修饰操作数的,表示该操作是可写的,意思是可改变的
    // 而input中,没有,是默认,表示可读
}

2.2.2 内存约束

c中每一个变量都对应一个内存地址,而内存约束指的就是,在内联汇编中,是需要将该变量取出来存到寄存器操作,还是直接以偏移地址的实行处理。

内存约束的含义其实是,类似与传指针的形式来传参。

内存约束要求gcc将位于input和output中的c变量的内存地址作为内联汇编代码的操作数,不需要寄存器做中转,直接进行内存读写,也就是汇编代码的操作数是c变量的指针。

  1. m表示操作数可以使用任意一种内存形式
  2. o表示操作数为内存变量,但访问他是通过偏移量的形式访问
void main()
{
    int in_a=1,in_b=2,out_sum;
    asm("movb %b0,%1"::"a"(in_a),"m"(in_b));
    // 0,1 分别表示,在output部分和input部分中从左到右使用的占位符
    // m 则表示使用地址传参
}

2.2.3 立即数约束

立即数就是常数,要求gcc传值的时候不通过内存和寄存器,直接作为立即数传给汇编代码,立即数不是变量,只能放在input中,这样也就是直接写死在代码中了。

  1. i操作数为整数立即数
  2. F操作数是富电力技术
  3. I范围0~31
  4. J范围0~63
  5. N范围0~255
  6. O范围0~32
  7. X操作数为任意类型的立即数

2.2.4 通用约束

0~9用在input部分,表示可与output和input中第n个操作数使用相同的寄存器或是内存

2.2.5 占位符

序号占位符

序号占位符是对pitput和input中的操作数,按照从左到右出现的次序从0开始编号,一直到9,最多支持10个操作数。使用的方法是%0等

操作数自身需要前面加%就是对操作数的引用。占位符只带约束对应的操作数。因为扩展内联汇编中占位符前要有前缀%,而寄存器前也有%,为了区别,所以寄存器前面再加一个%,一共两个%%.

占位符所表示的操作数默认是32位数据。当要使用寄存器的ah,也就是8~15位的时候,那么在%和数字之前加上h,来表示使用8~15位

asm("movw %1,%0;":"=m"(in_b):"a"(in_a));

名称占位符

名称占位符需要在output和input中吧操作数显式的起个名字,他用这样的格式来表示操作:[名称]"约束名"(c变量)

在汇编代码中使用的时候,用%[名称]来使用。

asm("movw %[a],%[b];":[b]"=m"(in_b):[a]"a"(in_a));

2.2.6 修饰操作数操作数的约束有

=表示操作数只写,只用在output中表示变量只写:=a(var_c)翻译为人话就是:var_c=eax

+表示操作数可读写,高速gcc所约束的寄存器或内存写呗读入,再被写入。也只用在output中,可以读写

&表示output中操作数要独占一个寄存器,只output使用,任何input部分不能与之相同

%该操作数可以和下一个操作数互换

一般input中c变量是只读的,output中的变量是只写的

2.3 扩展内联汇编-机器模式

机器模式用来在机器层面指定数据的大小和格式,由于各种约束不能确切的表达具体的操作数对象,所以引入机器模式,用来从更细的粒度上描述数据对象的大小以及其指定部分。

3 代码

目录结构:

└── bochs
├── 02.tar.gz
├── 03.tar.gz
├── 04.tar.gz
├── 05a.tar.gz
├── 05b.tar.gz
├── 06a.tar.gz
├── 06b
│   ├── boot
│   │   ├── include
│   │   │   └── boot.inc
│   │   ├── loader.asm
│   │   └── mbr.asm
│   ├── build
│   ├── kernel
│   │   └── main.c
│   ├── lib
│   │   ├── kernel
│   │   │   ├── io.h
│   │   │   ├── print.asm
│   │   │   └── print.h
│   │   └── libint.h
│   └── start.sh
└── hd60m.img

3.1 io.h

#ifndef __LIB_IO_H
#define __LIB_IO_H
#include "libint.h"

// 向 port 端口中写入数据
static inline void outb(uint16_t port,uint8_t data)
{
    asm volatile("outb %b0,%w1"::"a"(data),"Nd"(port));
    // outb是因为,AT&T语法中指令后面要跟传输的数据的大小,b表示1个字节
    // %b0,%w1 表示要传送的数据的大小
    // a表示使用a系列寄存器
    // Nd表示,N是0~255的整数,d表示使用edx系列寄存器,必须要这个
}

// 向端口中写入 counts 个字节
static inline void outsw(uint16_t port,const void *addr,uint32_t counts)
{
    asm volatile(
        "cld;rep outsw "
        :"+S"(addr),\
        "+c"(counts)\
        :"d"(port)
    );
    // 使用的 rep 指令,重复执行如下命令 ecx 次,
    // 方向由 cld 设定了。
}

static inline uint8_t inb(uint16_t port)
{
    uint8_t data;
    asm volatile ("inb %w1,%b0":"=a"(data):"Nd"(port));
    return data;
}

static inline void insw(uint16_t port, void* addr, uint32_t counts)
{
    asm volatile (
        "cld;rep insw;"\
        :"+D"(addr),\
        "+c"(count)\
        :"d"(port)\
        :"memory"
    );
}
#endif

猜你喜欢

转载自www.cnblogs.com/perfy576/p/9119176.html