使用库函数 API 和 C 代码中嵌入汇编代码两种方式使用同一个系统调用
一、内容学习
1、用户态、内核态和中断
-
内核态:处于高的执行级别下,代码可以执行特权指令,访问任意的物理地址,这时的CPU就对应内核态
-
用户态:处于低的执行级别下,代码只能在级别允许的特定范围内活动。在日常操作下,执行系统调用的方式是通过库函数,库函数封装系统调用,为用户提供接口以便直接使用。
-
Intel x86 CPU有四种不同的执行级别0-3,Linux只使用了其中的0 3级分别表示内核态和用户态。cs寄存器的最低两位表明了当前代码的特权级,00或者11。
-
内核态cs:eip的值是任意的,即可以访问所有的地址空间。用户态只能访问其中的一部分内存地址(0x00000000-0xbbbbbbbf),0xc0000000以上的地址(逻辑地址而不是物理地址)只能在内核态下访问。
-
中断处理是从用户态进入内核态的主要方式,系统调用是一种特殊的中断。从用户态切换到内核态时,中断/int指令会在堆栈上保存用户态的寄存器上下文,其中包括用户态栈顶地址、当时的状态字、当时的cs:eip的值,还有内核态的栈顶地址、内核态的状态字、中断处理程序的入口。中断发生后的第一件事就是保存现场,保存一系列的寄存器的值;中断处理结束前的最后一件事就是恢复现场,退出中断程序,恢复保存寄存器的数据。特别说明: 保护现场:就是进入中断程序,保存需要用到的寄存器的数据;恢复现场:就是退出中断程序,恢复保存寄存器的数据。
2、系统调用概述
-
系统调用的意义:操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用。把用户从底层的硬件编程中解放出来,极大的提高了系统的安全性,使用户程序具有可移植性。
-
API和系统调用:API是一个系统调用封装成的一个函数定义;系统调用通过软中断向内核发出一个明确的请求;Libc库定义的一些API引用了封装例程,目的是发布系统调用,让程序员写代码的时候可以通过函数调用而非汇编指令触发一个系统调用;一般每个系统调用对应一个封装例程,库再用这些封装例程定义出给用户的API。
-
应用编程接口(application program interface, API) 和系统调用是不同
-
不是每个API都对应一个特定的系统调用。API可能直接提供用户态的服务,比如一些数学函数;一个单独的API可能调用几个系统调用;不同的API可能调用了同一个系统调用。
-
系统调用的三层皮:xyz(API)、system_call(中断向量)、sys_xyz(中断服务程序)
-
内核实现了很多不同的系统调用, 进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数(使用eax寄存器来传递)
-
寄存器传递参数具有如下限制: 1)每个参数的长度不能超过寄存器的长度,即32位 2)在系统调用号(eax)之外,参数的个数不能超过6个(ebx, ecx,edx,esi,edi,ebp)
-
超过6个怎么办?超过6个的话就把某一个寄存器作为一个指针,指向某一块内存。
-
API-int 0x80 陷入内核态-systemcall-调用函数
二、实验过程和分析
1、系统函数调用——time
在自己的虚拟机上重现使用库函数 API 和 C 代码中嵌入汇编代码两种方式使用time系统调用,
代码如下图所示。
如果直接按照视频中所给的命令
gcc test_time.c -o time -m32
,则会发生段错误(核心已转储)的问题,如下图所示:
产生该错误的原因一般有以下几点:
- 内存访问越界
- 多线程程序使用了线程不安全的函数
- 多线程读写的数据未加锁保护。
- 非法指针
- 堆栈溢出
考虑本地虚拟机是64位环境,那么在64位linux下编译32位程序则可能是因为指针问题(暂时还不知道为什么会出现这样的错误)。但是如果在编译时不加-m32
选项则会导致输出的结果不对,原因可能是因为代码中tt
为64位,而最后赋给tt
值的eax
为32位,导致出现错误,当把time_t tt;
改为time_t tt=0;
后则编译运行的结果正确,如下图所示:
另一种方案是将汇编代码中使用的寄存器改为rax、rbx等64位寄存器,代码如下:
#include <stdio.h>
#include <time.h>
int main()
{
time_t tt;
struct tm *t;
asm volatile(
"mov $0,%%rbx\n\t"
"mov $0xd,%%rax\n\t"
"int $0x80\n\t"
"mov %%rax,%0\n\t"
:"=m"(tt)
);
t=localtime(&tt);
printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
return 0;
}
执行结果如下图所示:
这样的修改方式的运行结果也印证了自己的想法,如有错误,还请老师指正。
2、系统函数调用——chmod
第二个实验内容将使用chmod系统调用编写代码,实现的具体命令为z_chmod mode filepath
,通过该命令实现修改文件权限的功能。首先使用man
命令看一下chmod
的相关信息,如下图所示:
可以看到具体的
chmod
函数有两个参数filepath
(文件路径)和mode
(模式),其中对于模式的解释如下图所示:
通过查阅资料和查看chmod的介绍可以看出,函数chmod在解释mode时是当成8进制的去解释,因此传入该数值时需要按8进制传参,代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
int main(int argc, char* argv[])
{
printf("%s %s %s\n",argv[0],argv[1],argv[2]);
int mode,mode_user,mode_group,mode_other;
if(argc < 3) //缺少参数
{
printf("Lack of parameter\n");
exit(0);
}
mode = atoi(argv[1]); //将传入的mode字符串转化为10进制数值
printf("mode:%d\n",mode);
if(mode > 777 || mode < 0) //简单的限制mode范围,没有更加精细的限制,例如688
{
printf("Mode %d is invalid\n",mode);
exit(0);
}
//提取mode的uer、group、other各位设定的权限值
mode_user = mode / 100;
mode_group = (mode - mode_user*100)/10;
mode_other = mode%10;
mode = mode_user * 8 * 8 + mode_group * 8 + mode_other;
char *filepath = argv[2];
int flag=0;
//调用系统函数chmod
asm volatile(
"mov %2,%%ebx\n\t" //将第一个参数filepath传入ebx
"mov %1,%%ecx\n\t" //将第二个参数mode传入ecx
"mov $0x0f,%%eax\n\t" //将系统调用号传入eax
"int $0x80\n\t"
"mov %%eax,%0\n\t" //取返回值
:"=m"(flag)
:"c"(mode),"b"(filepath)
);
printf("flag:%d\n",flag);
if(flag == -1)
{
printf("%s unable to access files\n",filepath);
exit(0);
}
return 0;
}
测试结果如下图所示,可以看到成功修改文件权限。
三、实验总结
通过本次实验,动手实践了在C语言中使用汇编嵌入方式使用系统调用,再次学习了如何查找相关命令的使用规则以及系统调用函数的说明。在实践chmod过程中,对于mode的解析、参数的传递以及参数的顺序等有了进一步理解。