Skip to content

Latest commit

 

History

History
146 lines (94 loc) · 8.36 KB

Lab4使用库函数 API 和 C 代码中嵌入汇编代码两种方式使用同一个系统调用.md

File metadata and controls

146 lines (94 loc) · 8.36 KB

使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用

使用SSH连接starfive visionfive 2

在第一个实验中,我们配置好了使用 ssh 连接 starfive visionfive 2 开发板的流程。在连接好网线后,通过使用浏览器登录网管地址查看开发板的ip地址后,使用以下命令连接 starfive visionfive 2 开发板。

注意:192.168.xxx.xxx Ip地址需要使用自己查阅的Ip地址。

回车后,输入密码: starfive。进入 starfive visionfive 2 开发板中的shell。如下图所示:

image-20231105133005556

使用man查看write函数

使用以下命令查看write函数的具体用法

man 2 write

具体用法如图所示:

image-20231105133327813

write函数有三个参数。第0个参数,输出位置;第1个参数,指向输出内容的指针;第2个参数,输出内容的长度。

C语言调用write函数

接下来,我们将在shell中vim编写local_write.c程序,通过使用C语言调用write函数。

首先我们需要下载 vim, 使用以下命令下载安装 vim

sudo apt install vim

安装好后,使用以下命令创建 local_write.c 文件。

vim loacl_write.c

将下面的代码复制到 local_write.c 中:

#include <stdio.h>
#include <unistd.h>
int main(void){
	char s[]="hello, world.\n";
	write(1,s,13);
	return 0;
}

使用以下命令进行编译和运行:

gcc -o loacl_write -c local_wirte.c
./local_write

显示以下信息说明运行成功:

image-20231105134051213

RISC-V内联汇编调用write函数

接下来,我们将使用RISC-V内联汇编嵌入C语言代码中,调用write函数。

使用以下命令创建 asm_write.c 文件。

vim asm_write.c

将下面的代码复制到 asm_write.c 中:

#include <stdio.h>
#include <unistd.h>

int main() {
    char s[] = "hello, world\n";

    __asm__ volatile(
		    "li a2, 13\n"
		    "li a0, 1\n"
		    "mv a1, %[str]\n"
	    	"li a7, 64\n"	
		    "ecall \n"
		    :
		    : [str] "r" (s)	
    );

    return 0;
}

使用以下命令进行编译和运行:

gcc -o asm_write -c asm_write.c
./asm_write

显示以下信息说明运行成功:

image-20231105134323745

内联汇编解释

可能最开始有很多不了解的人会认为系统调用和函数调用差不多,然而当我们用汇编代码运行了第一个系统调用之后就会发现这个步骤和函数调用还是很不同的,即使他们在你是使用库函数的时候感觉是差不多的。它没有函数的入口,也没有函数的出口(请看lab2中的资料),而且还多了一个ecall。那么系统调用是怎么运行的呢?

抽象系统资源

为了完全明白这段汇编代码,让我们先了解一点前置的知识。

当谈及操作系统时,人们可能会问的第一个问题是为什么需要它?也就是说,我们可以将所有的系统调用实现为一个库,应用程序可以与之链接。在此方案中,每个应用程序甚至可以根据自己的需求定制自己的库。应用程序可以直接与硬件资源交互,并以应用程序的最佳方式使用这些资源(例如,实现高性能或可预测的性能)。一些嵌入式设备或实时系统的操作系统就是这样组织的。

这种库函数方法的缺点是,如果有多个应用程序在运行,这些应用程序必须表现良好。例如,每个应用程序必须定期放弃中央处理器,以便其他应用程序能够运行。如果所有应用程序都相互信任并且没有错误,这种协同操作的分时方案可能是可以的。 然而更典型的情况是, 应用程序互不信任且存在bug,所以人们通常希望提供比合作方案更强的隔离。

为了实现强隔离, 最好禁止应用程序直接访问敏感的硬件资源,而是将资源抽象为服务。 例如,Unix应用程序只通过文件系统的open、read、write和close系统调用与存储交互,而不是直接读写磁盘。这为应用程序提供了方便实用的路径名,并允许操作系统(作为接口的实现者)管理磁盘。即使隔离不是一个问题,有意交互(或者只是希望互不干扰)的程序可能会发现文件系统比直接使用磁盘更方便。

同样,Unix在进程之间透明地切换硬件处理器,根据需要保存和恢复寄存器状态,这样应用程序就不必意识到分时共享的存在。这种透明性允许操作系统共享处理器,即使有些应用程序处于无限循环中。

另一个例子是,Unix进程使用exec来构建它们的内存映像,而不是直接与物理内存交互。这允许操作系统决定将一个进程放在内存中的哪里;如果内存很紧张,操作系统甚至可以将一个进程的一些数据存储在磁盘上。exec还为用户提供了存储可执行程序映像的文件系统的便利。

linux系统调用接口是精心设计的,既为程序员提供了便利,又提供了强隔离的可能性。Unix接口不是抽象资源的唯一方法,但它已经被证明是一个非常好的方法

用户态,内核态,以及系统调用

强隔离需要应用程序和操作系统之间的硬边界,如果应用程序出错,我们不希望操作系统失败或其他应用程序失败,相反,操作系统应该能够清理失败的应用程序,并继续运行其他应用程序,要实现强隔离,操作系统必须保证应用程序不能修改(甚至读取)操作系统的数据结构和指令,以及应用程序不能访问其他进程的内存。

CPU为强隔离提供硬件支持。例如,RISC-V有三种CPU可以执行指令的模式:机器模式(Machine Mode)、用户模式(User Mode)和管理模式(Supervisor Mode)。在机器模式下执行的指令具有完全特权;CPU在机器模式下启动,OpenSBI也是运行在机器模式下的。操作系统运行在管理模式,用户程序运行在用户模式。

在管理模式下,CPU被允许执行特权指令:例如,启用和禁用中断、读取和写入保存页表地址的寄存器等。如果用户模式下的应用程序试图执行特权指令,那么CPU不会执行该指令,而是切换到管理模式,以便管理模式代码可以终止应用程序,因为它做了它不应该做的事情。应用程序只能执行用户模式的指令(例如,数字相加等),并被称为在用户空间中运行,而此时处于管理模式下的软件可以执行特权指令,并被称为在内核空间中运行。在内核空间(或管理模式)中运行的软件被称为内核。

想要调用内核函数的应用程序(例如上面的write系统调用)必须过渡到内核。CPU提供一个特殊的指令,将CPU从用户模式切换到管理模式,并在内核指定的入口点进入内核(RISC-V为此提供ecall指令)。一旦CPU切换到管理模式,内核就可以验证系统调用的参数,决定是否允许应用程序执行请求的操作,然后拒绝它或执行它。由内核控制转换到管理模式的入口点是很重要的;如果应用程序可以决定内核入口点, 那么恶意应用程序可以在跳过参数验证的地方进入内核。

write调用的汇编代码

现在我们已经明白了使用系统调用的原因。那么理解起来这段汇编代码就很简单了。首先我们需要将参数填入合适的位置,write系统调用的第一个参数是写在哪,需要我们传入文件描述符。第二个参数是写入的字符串的指针。第三个参数是写入字符串的长度。我们将每个值按照相应的方法"li a2, 13\n" "li a0, 1\n" "mv a1, %[str]\n"加载进寄存器(这部分是和函数调用是一样的),然后接下来我们需要陷入到内核态,在RISCV里面我们使用的是ecall。我们将ecall写上,但是此时系统并不知道我们是什么系统调用,所以我们需要在a7寄存器里面加入我们的系统调用号64。自此系统调用的代码我们就完成了。