操作系统——内联汇编(十三)
操作系统——内联汇编(十三)
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的使用格式却相当的简单,只需要使用双引号将寄存器的名称直接包含在内即可,多个寄存器之间需要使用逗号“,”进行间隔。