一、知识点
1.系统调用
系统调用(System Call)是操作系统提供给应用程序的接口,用于访问操作系统的服务和资源。应用程序通过系统调用向操作系统请求执行特权操作,例如文件操作、网络通信、进程管理等。
应用程序在用户态下执行时,它们无法直接访问操作系统的内核功能和资源。为了获取操作系统提供的服务,应用程序需要通过系统调用将控制权转移到内核态,让操作系统代表应用程序执行相应的操作。系统调用提供了一种安全机制,确保应用程序只能访问其被授权的功能和资源,同时保护操作系统的稳定性和安全性。
系统调用的执行过程通常包括以下步骤:
应用程序发起系统调用:应用程序通过特定的系统调用指令(例如int 0x80或syscall指令)或高级语言提供的接口,传递系统调用号和参数给操作系统。
切换到内核态:当应用程序发起系统调用时,处理器会将当前的执行环境从用户态切换到内核态,即从用户空间切换到内核空间。这个切换是由操作系统的特权级别和硬件机制来实现的。
系统调用处理:操作系统根据系统调用号确定需要执行的操作,并根据参数执行相应的服务。系统调用处理程序在内核态下执行,它会验证参数的合法性、执行相应的操作,并返回结果给应用程序。
返回结果到应用程序:系统调用处理程序执行完毕后,将结果返回给应用程序。处理器将执行环境从内核态切换回用户态,应用程序继续执行。
2.用户态
用户态是指进程或线程在执行时的一种受限权限级别。在用户态下,进程或线程只能访问受限的系统资源和执行受限的指令集。它们不能直接访问操作系统的核心功能或直接操作硬件设备。用户态下的程序通常运行在用户空间,例如运行应用程序、执行用户代码等。
3.内核态
内核态是指操作系统内核运行时的权限级别。在内核态下,操作系统具有完全的权限和访问权,可以执行任意指令,访问系统的所有资源和设备,以及执行特权指令。内核态下的程序通常运行在内核空间,它们负责管理和控制系统资源,处理中断请求,提供系统服务等。
用户态和内核态之间的切换是通过系统调用(System Call)或异常(Exception)实现的。当进程或线程需要执行操作系统提供的服务或访问受限资源时,它们需要通过系统调用将控制权转移到内核态,让操作系统代表它们执行相应的操作。完成操作后,控制权再次返回用户态。
4.中断
中断是计算机系统中的一种事件,它会打断正在执行的程序或指令,并引发系统响应。中断可以由硬件设备、外部事件或软件触发。当发生中断时,系统会暂停当前正在执行的任务,保存当前状态,然后转移到中断处理程序执行相应的操作。中断处理程序通常在内核态下执行,它们负责处理中断事件,提供相应的服务和处理中断请求。处理完中断后,系统会恢复中断前的状态,并继续执行被中断的任务。
二、实验过程
1、系统函数调用——time
在自己的虚拟机上重现使用库函数 API 和 C 代码中嵌入汇编代码两种方式使用time系统调用,
代码如下图所示
按照如下代码运行
gcc test_time.c -o time -m32
运行后发现出现段错误,核心已转储
原因一般有以下几点:
非法内存访问:段错误最常见的原因是试图访问未分配给程序的内存区域或者试图读取或写入非法的内存地址。这可能是由于指针错误、数组越界、堆栈溢出或者使用已释放的内存等导致的。
空指针解引用:当程序中的指针为NULL或未初始化时,尝试通过该指针解引用读取或写入内存时,会导致段错误。
栈溢出:如果在函数调用过程中使用了大量的局部变量或递归调用层次过深,可能会导致栈溢出,进而引发段错误。
动态内存管理错误:在使用动态内存分配函数(如malloc、free等)时,如果出现错误的内存分配或释放操作,可能会导致段错误。
库或依赖问题:有时,段错误可能是由于与程序相关的库或依赖项发生冲突或错误引起的。这可能包括库版本不匹配、库文件损坏或依赖项未正确安装等情况。
并发或线程问题:在多线程或并发编程中,如果没有正确地同步对共享资源的访问,可能会导致段错误。
解决办法:
检查代码:仔细检查代码,特别是与内存访问相关的部分,查找可能导致段错误的错误操作。
使用调试工具:使用调试器(如gdb)来跟踪程序的执行过程,定位段错误发生的具体位置,以及找出引发错误的原因。
检查指针和内存管理:确保指针的正确初始化和使用,避免空指针解引用和非法内存访问。注意动态内存分配和释放的正确性。
检查并发或线程问题:如果程序涉及多线程或并发操作,确保正确地同步对共享资源的访问,避免竞态条件。
检查库和依赖项:确保库和依赖项的正确安装和版本匹配,排除库冲突或损坏的可能性。
日志和错误处理:在程序中添加适当的日志记录和错误处理机制,以便更好地定位和处理段错误。
将汇编代码中使用的寄存器改为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在解释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;
}
编写完后使用下面函数编译运行
gcc -g mod.c -o mod -m32
ls -l
测试结果如下图所示,可以看到成功修改文件权限
1.编译运行
2.修改文件权限,前后对比修改成功
三、实验心得
本次实验对于系统调用,用户态还有内核态有了一定了解,对于chmod指令还有函数传参,汇编等一系列操作更加熟悉,对于自己linux的学习有了很大帮助,下一步还是要重视一些细节,不要在某些地方浪费太多时间,提升学习效率。