使用Rust开发操作系统(Rust内联汇编)

Rust内联汇编

Rust的内联汇编基础语法如下(需要启用#!(feature(asm)))


asm!(
	assembly template 
	: 输出操作数
	: 输入操作数
	: Clobber
	: 选项
);

assembly template

assembly template是唯一需要的参数并且必须是原始字符串例如asm!("nop"),该指令不需要任何参数,因此省略了

因为Rust内联汇编还处于Unstable,因此我们需要使用#![feature(asm)]放在lib.rs文件对开头部分如下

system/src/lib.rs
#![no_std]
#![feature(asm)]

pub mod bits;
pub mod mutex;
pub mod ia_32e;


#[cfg(test)]
mod tests;

使用时需要添加unsafe块或函数需要添加unsafe关键字,例如

pub unsafe fn nop(){
	asm!(
	"xor %eax, %eax"
	:
	:
	: "{eax}"
	:
	);
}
或者
pub  fn nop(){
	unsafe{
		asm!(
		"xor %eax, %eax"
		:
		:
		: "{eax}"
		:
		);
	}
}


在调用unsafe函数时需要使用unsafe块

fn main(){
	unsafe{
		nop();
	}
}

如果一个函数只有一个内联汇编操作的话,建议将该函数声明为unsafe的,为了提高执行效率可以在函数上添加#[inline]宏(与C语言的#inline宏类似),这在编译时起到了优化的效果

#[inline]
pub unsafe fn xor(){
	// 有空格也没关系
	asm!("xor %eax, %eax" ::: "{eax}");
}

在调用该函数时我们可以添加#[cfg(target_arch = "x86_64")]来指定要编译的目标系统的架构,

模板字符串支持使用$后跟一个数字的参数替换例如$0,以指示由约束字符串指定的给定寄存器/内存位置的替换。${NUM:MODIFIER}也可以使用,其中MODIFIER是如何打印操作数的特定于目标的注释

字符$可以在模板中使用$$。要在输出中包含其他特殊字符,可以使用通常的“\XX”转义符,就像在其他字符串中一样。

约束

约束列表是逗号分隔的字符串,每个元素包含一个或多个约束代码,例如:“约束1”(表达式1),“约束2”(表达式2)...

对于约束列表中的每个元素,将选择一个适当的寄存器或内存操作数,并且将对$0列表中的第一个约束,$1第二个等将使其可用于组件模板字符串扩展。

输出约束

输出约束由“=”前缀(例如“=r”)指定。这表示程序集将写入此操作数,然后操作数将作为asm表达式的返回值提供。输出约束不会消耗调用指令中的参数。

LLVM输出约束的原文如下

通常,在读取所有输入之前,预计没有输出位置被汇编表达式写入。因此,LLVM可以将相同的寄存器分配给输出和输入。如果这不安全(例如,如果程序集包含两条指令,其中第一条写入一个输出,第二条读取输入并写入第二条输出),则必须使用“&”修饰符(例如“=&r”)来指定输出是“早期破坏”输出。将输出标记为“early-clobber”可确保LLVM不会对任何输入(除了与此输出关联的输入)使用相同的寄存器。

例如:
将cs寄存器的值移动到ax变量中

let ax: u16;
asm!(
	"movw %cs, %ax"
	: "={ax}"(ax)
	:
	:
)

输入约束

输入约束没有前缀 只是约束代码。每个输入约束将从调用指令中消耗一个参数。asm不允许写入任何输入寄存器或存储单元(除非该输入连接到输出)。还要注意,如果LLVM可以确定它们必然都包含相同的值,则可以将多个输入全部分配给相同的寄存器。

通过提供一个整数作为约束字符串,输入约束可以将它们自己绑定到输出约束,而不是提供约束代码。被绑定的输入仍然会从调用指令中消耗一个参数,并且按照通常的方式在asm模板编号中占据一个位置

它们将被简单地限制为始终使用与其绑定的输出相同的寄存器。例如,一个约束字符串“=r,0”表示为输出分配一个寄存器,并将该寄存器用作输入(它是第0个约束)

例如,将0x23移动到ss寄存器

asm!(
	"movw $0, %ss"
	:
	: "r"(0x23)
	: "memory"
)

指定寄存器名,可以使用多个参数

asm!("outb %al,%dx"
	: 
	:"{dx}"(0x21),"{al}"(0x21)
	:
)

所有目标通常都支持一些约束代码:

约束解释
r目标通用寄存器类中的寄存器
m存储器地址操作数。它支持哪些寻址模式,典型的例子是寄存器,寄存器+寄存器偏移量,或寄存器+直接偏移量(某些目标特定的大小)
i一个整数常量(目标特定宽度)。允许简单的即时或可重定位的值
n一个整数常量 – 不包括可重定位值
s一个整数常量,但只允许重定位值
X允许任何类型的操作数,不受任何限制。通常用于为asm分支或call传递标签
{register-name}需要完整的指定物理寄存器

Clobber约束

clobber不会消耗输入操作数,也不会输出操作数。

一些指令修改的寄存器可能保存有不同的值,所以我们使用覆盖列表来告诉编译器不要假设任何装载在这些寄存器的值是有效的

“memory”表示程序写入任意未声明的内存位置 不仅是由声明的间接输出指向的内存。

请注意,输出约束中存在的clobbering命名寄存器是不合法的。

约束代码可以是单个字母(例如“r”),“^”字符后跟两个字母(例如“^wc”)或“{”寄存器名称“ }”(例如“{eax}”)。

通常选择单字母和双字母约束代码与GCC的约束代码相同

一些指令修改的寄存器可能保存有不同的值,所以我们使用覆盖列表来告诉编译器不要假设任何装载在这些寄存器的值是有效的

options

最后一部分,options是 Rust 特有的。格式是逗号分隔的基本字符串(也就是说,:“volatile”, “intel”, “alignstack”)。它被用来指定关于内联汇编的额外信息:

目前有效的选项有:

  • volatile - 相当于 gcc/clang 中的__asm__ __volatile__ (...)
  • alignstack - 特定的指令需要栈按特定方式对齐(比如,SSE)并且指定这个告诉编译器插入通常的栈对齐代码
  • intel - 使用 intel 语法而不是默认的 AT&T 语法

例如使用Intel语法编写内联汇编

 asm!(
	 "mov eax, 2" 
	 : "={eax}"(result) 
	 : 
	 : 
	 : "intel"
 )

更多例子

实际使用的代码中包含了很多实际使用到的内嵌汇编代码

开始干活

输了Rust内联汇编的基础用法后,我们开始着手编写函数方便以后使用
我们在上一篇文章中已经编写了一个加载GDT的指令lgdt

pub unsafe fn lgdt(gdt: &DescriptorTablePointer) {
    asm!(
    	"lgdt ($0)" 
    	:
    	: "r" (gdt) 
    	: "memory"
    );
}

在该内联汇编中我们使用了输入约束和Clobber约束,我们是需要往内存中写入数据的,在Clobber约束中指定了memory约束

接下来我们编写加载寄存器的操作
src/ia_32e/instructions/segmention.rs文件中添加以下内容

pub unsafe fn set_cs(selector: SegmentSelector) {
    #[inline(always)]
    unsafe fn inner(selector: SegmentSelector) {
        asm!(
            "pushq $0;\
            leaq 1f(%rip), %rax;\
            pushq %rax;\
            lretq;\
            1:"
            :
            : "ri"(u64::from(selector.0))
            : "rax" "memory"
        );
    }
    inner(selector);
}

set_cs函数用来设置cs寄存器的内容(用于设置代码执行位置,比如在进程调度中需要保存上一个进程的执行位置并设置下一个进程的执行位置,我们就可以通过这个函数来完成),set_cs函数中的汇编指令比较多,我们先来认识以上的几个指令:

  • PUSH: push指令的功是将RSP寄存器进行递减,然后将源操作数存储在堆栈顶部,PUSH后面的Q表示Quadword(四字)后面leaqlretq同理
  • LEA: lea指令(Load Effective Address,加载有效地址)的功能是取源操作数地址的偏移量,并把它传送到目的操作数所在的单元
  • LRET: lret指令(Long Return,远返回)将控制权转移到位于堆栈上的返回地址,该地址通常由lcall指令放置在堆栈中。在被调用的过程中发出lret指令,以在调用之后的指令处恢复执行流程

我们使用了#[inline(always)要求编译器必须内联inner函数,inner看似多次一举其实不然,我们没用直接使用mov指令去设置cs,而是将新的段选择子压入栈中并且使用lretq重新加载cs寄存器,并在1:处继续,在约束中我们指定了ir表示指定一个用于存储整数常量的通用寄存器,我们在汇编中对rax做了改动并且有内存写入操作,因此在Clobber约束中增加了raxmemory

然后我们对ds,fs,gs,es寄存器提供同样的操作,我们加载这些寄存器的时候就不像cs寄存器那么复杂了
代码如下

/// 加载ss段选择子
pub unsafe fn load_ss(selector:SegmentSelector){
    asm!(
        "movw $0, %ss"
        :
        : "r"(selector.0)
        :"memory"
    );
}

/// 加载ds段选择子
pub unsafe fn load_ds(selector:SegmentSelector){
    asm!(
        "movw $0,%ds"
        :
        :"r"(selector.0)
        :"memory"
    );
}
/// 加载es段选择子
pub unsafe fn load_es(selector:SegmentSelector){
    asm!(
        "movw $0,%es"
        :
        :"r"(selector.0)
        :"memory"
    );
}

/// 加载fs段选择子
pub unsafe fn load_fs(selector:SegmentSelector){
    asm!(
        "movw $0, %fs"
        :
        :"r"(selector.0)
        :"memory"
    );
}

/// 加载gs段选择子
pub unsafe fn load_gs(selector:SegmentSelector){
    asm!(
        "movw $0, %gs"
        :
        :"r"(selector.0)
        :"memory"
    );
}

除了加载之外我们需要提供一个获取当前cs寄存器的值的方法(例如用于进程调度时获取当前进程的执行位置)

/// 获取当前的代码段选择子
pub fn cs() -> SegmentSelector{
    let segment:u16 = 0;

    unsafe{
        asm!(
            "mov %cs, $0"
            :"=r"(segment)
        );
    }
    SegmentSelector(segment)
}

这样我们的关于段的内联汇编操作就完成了!

下一步要做什么

在下一篇文章中我们需要认识TSS(Task State Segment)并且实现Tss结构提供TSS加载的方法!


版权声明:本文为qq_41698827原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。