操作系统——内联汇编(十三)

操作系统——内联汇编(十三)

2020-09-26 17:33:27 hawk


概述

  这里我们简单介绍一下内联汇编这项技术,方便我们后面再内核的c代码中直接添加汇编部分指令。这部分还是比较枯燥,对于有基础的或不感兴趣的,可以先行跳过,等到需要的时候在重新进行查阅即可。


定义

  内联汇编称为inline assembly。一般情况下,c语言不支持寄存器操作,但是汇编语言。因此往往会向c语言中添加内联汇编,实现更多的功能。

AT&T语法

  我们前面一直使用的汇编代码格式是Intel语法的,但是gcc中只支持AT&T格式的内联汇编,这里简单对比一下intel和AT&T汇编的风格

区别 Intel AT&T
寄存器 寄存器前无前缀 寄存器前有前缀%
操作数顺序 目的操作数在左,源操作数在右 源操作数在左,目的操作数在右
操作数指定大小

有关内存的操作数前要加载数据类型修饰符:

byte 8位;word 16位;dowrd 32位

指令的最后一个字母表示操作数大小:

b表示8位;w表示16位;l表示32位

立即数 无前缀 有前缀$
远跳转 jmp far segment:offset ljmp $segment:$offset
远调用 call far segment:offset lcall $segment:$offset
远返回 ret far n iret $n

 

  这里着重说明一点,即AT&T的内存寻址是有固定格式的,如下所示

segreg:base_address(offset_address, index, size)

  其中,base_address是基地址,可以是整数、变量名,可正可负;offset_address是偏移地址,而index是索引值,这两个要求必须是8个通用寄存器之一;size是尺度,只能是1、2、4、8等。这个表示的实际地址是segreg:base_address + offset_address + index * size。

 基本内联汇编

  下面我们简单介绍一下gcc中支持的基本的内联汇编。其格式如下所示

asm    [volatile]    ("assembly code")

  其中,asm是关键字,用于生命内联汇编表达式,这是内联汇编固定的部分,不可少,这里多说一下,asm和__asm__实际上是一样的;关键字volatile是一个可选项,避免gcc优化代码的时候修改下面的汇编代码部分,同样,volatile和__volatile__是一样的。

  整体上,只要满足了上面介绍的两部分,即使汇编代码为空,gcc同样可以正常编译。下面再介绍一下assembly code的相关规则。

  1.  指令必须位于双引号之内,无论双引号内是一条指令,亦或是多条指令

  2.  一对双引号不能跨行,如果想要实现跨行效果,必须在结尾使用反斜杠‘\'进行转义

  3.  指令之间使用分号'";"、换行符"\n"或者换行符加制表符"\n\t"进行分隔。除去最后一条指令外,其余每条指令间都需要添加分隔符

 

  当然,内联汇编的话也可以使用c语言中定义的变量,如果仅仅是一般形式的话,则需要将变量定义为全局的,如下代码所示(注意下面都是64位的代码)

char* str = "Hawk's inline\n";

int main(void) {
    asm("\
\
    movq    $1, %rax;\
    movq    $1, %rdi;\
    movq    str, %rsi;\
    movq    $14, %rdx;\
    syscall");
    

    return 0;
} 

 

  这个是64位下的write的系统调用,然后我们编译执行,如下图所示

 

 扩展内联汇编

  实际上前面的功能还是比较有限的——比如只能使用定义好的全局变量。因此,人们对其进行了拓展,以实现更多的功能。

  实际上,编译器同样提供了扩展内联汇编的格式,如下所示

asm [volatile] ("assembly code":output : input : clobber/modify)

 

  可以看到,和基础的内联汇编相比较,多了output、input以及clobber/modify部分。这里需要额外说明一下,实际上括号中的每一部分都可以省略,但是省略部分仍然需要保留冒号分隔符进行占位——当然,如果省略的是最后一部分,则分隔符也可以不用保留。

  实际上,input和output正是c为汇编提供参数输入和结构输出的部分。对于output,其操作数的格式为

“操作数修饰符约束名"(c变量名)

  

  其中的引号和圆括号都不能少,并且操作数修饰符通常为等号“=”,多个操作数之间使用逗号”,“进行分隔;而对于input,其操作数的格式为

"[操作数修饰符] 约束名”(c变量名)

 

  其中的引号和圆括号同样都不能少,但是操作数修饰符为可选项。同样的,多个操作数之前同样使用逗号“,”进行分割。而最后的clobber/modify表示汇编代码执行后,会破坏一些内存或寄存器资源,这样gcc编译器就会提前将对应的资源进行保护。

  当然,这些交互不可能完全像c一样自由,下面是对于这些input/output指令的约束,主要可以分为四大类。

  1.  寄存器约束

    表示将input或output中的变量约束在某些寄存器中,常见的约束如下所示

寄存器约束 对应的寄存器
a eax/ax/al
b ebx/bx/bl
c ecx/cx/cl
d edx/dx/dl
D edi/di
S esi/si
q eax/ebx/ecx/edx四个通用寄存器之一
r eax/ebx/ecx/edx/esi/edi六个通用寄存器之一
g 任意地点
A 把eax和edx组合成64位整数
f 表示浮点寄存器
t 表示第1个浮点寄存器
u 表示第2个浮点寄存器

  下面我们修改上面的代码,将其转换为内联汇编格式,如下所示

int main(void) {
    char* str = "Hawk's inline\n";

    asm("\
    movq    $1, %%rax;\
    movq    $1, %%rdi;\
\
    movq    %%rbx, %%rsi;\
    movq    $14, %%rdx;\
    syscall"::"b"(str));
    

    return 0;
} 

 

   这里需要说一点——所有的寄存器都需要使用%%表示,单独的%有特殊的用处。然后我们编译运行上述的代码,如下所示

 

 

   这里说明一下赋值顺序,首先代码执行的时候,会将input部分的变量值首先赋给对应的寄存器,然后执行整个指令(向这幅图就是所有的指令)。当指令之后结束后,再将output中对应的寄存器赋值给对应的变量即可——也就是,input和outpub自然可以选择相同的寄存器,但仍然正常运行。

  2.  内存约束

  内存约束是要求gcc编译器将位于input或者output中的变量的内存地址作为内联汇编代码的操作数,从而直接进行内存的读写,不需要寄存器进行中转。其代码如下所示

int main(void) {
    char* str = "Hawk's inline\n";

    asm("\
    movq    $1, %%rax;\
    movq    $1, %%rdi;\
\
    movq    %0, %%rsi;\
    movq    $14, %%rdx;\
    syscall"::"m"(str));
    

    return 0;
} 

  

  这里面有一个占位符,也就是%0,这里先简单理解为一个操作值即可。然后下面进行编译运行,如图所示

 

 

 

  这里还是需要说明一下个人理解,这里我认为就是直接通过类似

mov  rsi, [ebp + offset]

  直接不通过额外的寄存器,直接通过内存访址,将值赋值给目标寄存器。前面的寄存器约束中,貌似中间通过额外寄存器。

  3.  立即数约束

  即要求gcc在传值的时候不通过内存和寄存器,直接作为立即数传递给汇编代码即可,其只能作为左值,也就是input中。其格式如下所示

立即数约束 立即数
i 整数立即数
F 浮点数立即数
I 0-31之间的立即数
J 0-63之间的立即数
N 0-255之间的立即数
O 0-32之间的立即数
X 任何类型立即数

  4.  通用约束

  即0-9,仅仅用在input部分,表示input和output中第n个操作数用相同的寄存器或内存。

占位符

  有时候我们对于寄存器要求并不严格,但是需要知道被分配的是哪个寄存器,从而对其进行操作;又或者我们进行内存约束时,无法指定寄存器,这里就需要用到占位符。占位符,即代表约束指定的操作数,方便对操作数的引用。目前占位符主要分为序号占位符和符号占位符。

  1.  序号占位符,即从0-9,对在output和input中操作数,按照从左到右的顺序进行编号,引用通过%0-9进行引用。

  2.  名称占位符,即在input和output中使用如下格式进行表示操作数

[名称] "约束名“ (c变量)

  引用通过%[名称]直接进行引用即可

修饰符

  前面的修饰符,主要就简单的介绍了一下output中的修饰符”=“,这里再稍微拓展的讲解一下。实际上在output中,修饰符主要有三种,”=“,”+“以及”&“这三种;而对于iinput,基本上就只有“%”。其中对于output来说,“=”表示操作数是只写;“+”表示操作数是可读写的,也就是如果input中和output中都需要改变该值,只需要定义在output中即可;而“&”表示output独占该寄存器,任何input中所分配的寄存器不能与之相同。

clobber/modify

  前面已经说过了,这里是用来标记可能因为执行内联的汇编代码而导致的环境改变。首先,对于input和output中的操作数,实际上gcc肯定已经知道,所以不需要在通过clobber/modify进行通知了,所以实际上需要通知的是不明显的资源破坏——比如因为调用函数导致的一些变化。但是clobber/modify的使用格式却相当的简单,只需要使用双引号将寄存器的名称直接包含在内即可,多个寄存器之间需要使用逗号“,”进行间隔。

posted @ 2020-09-26 21:14  hawkJW  阅读(699)  评论(0编辑  收藏  举报