2020-2021-1 20209317《Linux内核原理与分析》第五周作业

《linux内核原理与分析》第五周作业

一、实验相关

使用库函数API和C代码嵌入汇编实现time()系统调用

使用API实现time

time.c代码如下:

#include<stdio.h>
#include<time.h>

int main(){
	time_t tt;
	struct tm *t;
	tt = time(NULL);
	t = localtime(&tt);
	printf("time:%d:%d:%d:%d:%d:%d:\n",t->tm_year+1900,t->tm_mon+1,t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
	
	return 0;
	
}

问题:书中给出的结构体中的天的参数名错误,显示的月份差了一个月

问题截图:

查看localtime的代码定义发现月份是从0开始的,所以在显示的时候对月份进行加一,查阅tm结构体找到关于天的正确命名为tm_mday,修改后代码正确,
tm结构体代码如下:

struct tm
{    
	int tm_sec;  /*秒,正常范围0-59, 但允许至61*/    
	int tm_min;  /*分钟,0-59*/    
	int tm_hour; /*小时, 0-23*/    
	int tm_mday; /*日,即一个月中的第几天,1-31*/    
	int tm_mon;  /*月, 从一月算起,0-11 1+p->tm_mon;  */ 
	int tm_year;  /*年, 从1900至今已经多少年 1900+ p->tm_year;  */ 
	int tm_wday; /*星期,一周中的第几天, 从星期日算起,0-6*/    
	int tm_yday; /*从今年1月1日到目前的天数,范围0-365*/    
	int tm_isdst; /*日光节约时间的旗标*/
};

结果截图如下:

使用C语言内嵌汇编实现time

代码如下:

#include<stdio.h>
#include<time.h>

int main(){
	time_t tt;
	struct tm *t;
	asm volatile(
		"movl $0,%%ebx;\n\t"
		"movl $0xd,%%eax;\n\t"
		"int $0x80;\n\t"	
		"movl %%eax,%0;\n\t"
		:"=m"(tt)
		:
		:"eax","ebx"
	);
	t = localtime(&tt);
	printf("time:%d:%d:%d:%d:%d:%d:\n",t->tm_year+1900,t->tm_mon+1,t->tm_mday,t->tm_hour,t->tm_min,t->tm_sec);
	return 0;
}

问题:问题截图如下,按照书上的代码进行编译能通过,执行时会报一个段错误,查阅资料,该错误代表堆栈溢出,或者进行了不安全的操作,于是在汇编的最下面加入破坏性描述,代表“eax”“ebx”两个寄存器会被改变,最后执行顺利!

使用库函数和C语言内嵌代码实现write()系统调用

作为一个程序员,写的第一行代码都是输出“hello world”,所以我在做这部分自选内容的时候选择了输出hello信息。还记得本科学linux的时候,了解到linux系统会为每个进程打开标准输入(stdin)、标准输出(stdout)、标准错误信息(strerr)三个文件,而想要在控制台输出想要的内容,就是要在标准输出文件中添加内容,于是我使用man查阅了stdout和stdio的相关信息,如下图:


同时为了使用write系统调用,也是用man查阅了write函数调用的相关信息,如下图:

好了该了解的都了解了,那就开始动手吧!

使用库函数实现write()

代码如下:

#include<unistd.h>

int main(){
	char *message = "hello 20209317\n";
	write(1,message,15);
	return 0;
}

注意这里的头文件是 unistd.h 并不是我们通常使用的 stdio.h。编译并运行,结果如下图:

nice!完美运行!

使用C语言内嵌汇编代码实现write()

代码如下:

int main(){
	char *message = "hello 20209317\n";
	int length= 15;
	int ret;
	asm volatile(
		"movl $0x4, %%eax\n\t"     //write系统调用号为4
		"movl $0x1, %%ebx\n\t"     //文件描述符1:标准输出stdout
        	"movl %1, %%ecx\n\t"      //要输出的信息
        	"movl %2, %%edx\n\t"      //要输出的长度
        	"int $0x80\n\t"
        	"movl %%eax, %0\n\t"
        	:"=m"(ret)
        	:"c"(message),"d"(length)
                :
	);

}

这里write的系统调用号为4,另外需要三个参数分别放在ebx,ecx,edx中分别是文件描述符1,指向标准输出,要输出的信息,在这里指的是message字符数组的第一个字符的地址,要输出的长度,在这里直接给出。结果如下图:

运行完美成功!

二、学习收获

1、用户态、内核态、中断

来自书上的图完美表示了三者的关系

用户态和内核态

根据书上的知识,Linux操作系统的体系架构分为用户态和内核态(或者用户空间和内核)。内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。linux中采用0、3两个特权级别,0为内核态,3为用户态,内核态下可以访问所有的地址空间,但是在用户态下只能访问0x00000000-0xbfffffff的地址空间,0xc0000000以上的地址空间只能在内核态下访问。

中断

根据书上的知识,中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的 CPU 暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。Linux中通常分为外部中断(又叫硬件中断)和内部中断(又叫Trap)。系统调用就是一种内部中断,当用户态切换到内核态时,就要把用户态寄存器上下文保存起来,同时把内核态寄存器的值放到当前cpu中。

2、标准输入(stdin)、标准输出(stdout)、标准错误信息(stderr)

在字符终端环境中,标准输入/标准输出的概念很好理解。输入即指对一个应用程序或命令的输入,无论是从键盘输入还是从别的文件输入;输出即指应用程序或命令产生的一些信息;与 Windows 系统下不同的是,Linux 系统下还有一个标准错误输出的概念,这个概念主要是为程序调试和系统维护目的而设置的,错误输出于标准输出分开可以让一些高级的错误信息不干扰正常的输出信息,从而方便一般用户的使用。
在 Linux 系统中:标准输入(stdin)默认为键盘输入;标准输出(stdout)默认为屏幕输出;标准错误输出(stderr)默认也是输出到屏幕(上面的 std 表示 standard)。在 使用这些概念时一般将标准输入表示为0,标准输出表示为 1,将标准错误输出表示为 2,,而根据linux万物皆文件的理念,这三个数字又代表了系统分配的三个文件的文件描述符,因此可以通过在这三个文件读取、写入来获取输入和输出,最经常利用到这三个文件描述符的场景,是在使用输入输出重定向和使用管道的时候。

3、系统调用的具体操作和参数传递方式

通过汇编中的int 0x80来进行执行软中断即系统调用进入内核态。系统调用从用户态切换到内核态,在两种在执行模式下使用的是不同的堆栈,即进程的用户态和进程的内核态堆栈。切换时系统自动保存调用系统调用的进程的上下文到用户堆栈中,用户需要提前把系统调用相关参数准备好,第一个参数为系统调用号,放在EAX寄存器中,其余参数按顺序赋值给EBX、ECX、EDX、ESI、EDI、EBP中,参数的个数不能超过六个,即上述6个寄存器。如果超过6个就把某一个寄存器作为指针指向内存,这样就可以通过内存来传递更多的参数。当系统调用执行完之后,系统调用通过iret指令将用户进程的上下文回复,同时将系统调用的返回值放在EAX寄存器中,用户进程将继续执行。

posted @ 2020-11-02 22:46  20209317李明帅  阅读(162)  评论(0编辑  收藏  举报