学号084 原创作品转载请注明出处+ https://github.com/mengning/linuxkernel/
一、Linux-5.0.2版本内核编译
编译内核的过程充分证明了了解底层知识是多么的必要。在使用手工根目录过程和使用Busybox编译过程中遇到同样一个问题:
在网上搜集了相关的错误信息,有些博客说是因为未能检测到device,有些博客说是因为某个libxxx.so文件出现了问题,有些博客说是virtualbox的问题,但是解决办法都没有解决我的问题。无奈,只能不断尝试。最终,参考Linux内核编译与安装博客内容完成了内核5.0.2版本的编译。
实验环境: Ubuntu 16.04.6
内核版本:
编译过程与指令:
1.下载源码并安装
mv linux-5.0.2.tar.xz /usr/src
cd linux-5.0.2
cp /boot/config<tab> ./config
2.配置与编译内核
make menuconfig
make
make modules_install
make install
问题基本都集中于编译内核的过程中,常见的问题如下:
a.缺少bison库
sudo apt-get install bison
b.缺少flex库
sudo apt-get install flex
c.openssl出错
sudo apt-get install libssl-dev
其中处理openssl错误时,又出现了libssl-dev无法安装的问题。这里是因为libssl-dev需要使用aptitude 进行降级版本安装
sudo apt-get install aptitude
sudo aptitude install libssl-dev
经过较长时间的make时间,编译系统的难题集中点就被攻克了。
3.生成启动
mkinitramfs -o /boot/initrd.img-5.0.2
update-initramfs -c -k 5.0.2
update-grub2
重启后,再次查看内核版本:
至此,内核5.0版本编译工作基本完成!这次实验的问题主要集中于Exitcode:0x0000000b的问题,耗费了很多时间着实无奈。好在,终于能够编译成功,继续接下来的实验。了解硬件与底层知识,对于解决相关问题颇为重要。
二、系统调用跟踪分析
1.基础知识补充
首先通过查询/usr/include/asm/unistd_32.h
文件获取与学号对应的系统调用号及其所对应的函数。
84号对应的是oldlstat函数。 oldlstat主要包含stat、fstat、lstat,主要用于获取文件的状态。使用过程中需要使用头文件
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
其函数头的主要形式为:
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
这里我们再看一下struct stat
这个结构体的内容:
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};
可以看到结构体中定义了一堆与文件属性相关的内容,为了简化实验,这里我们针对st_size进行实验。
2.从孟老师的github上获取time()函数范例https://github.com/mengning/menu
3.参考time()函数范例,使用自己的函数调用号编写相关代码。
首先我们新建了Oldlstat()函数,设置一个整形变量用于记录lstat()函数存放在stat结构体中的相关参数。
int Oldlstat()
{
struct stat buf;
int num;
lstat("/etc/passwd", &buf);
num = buf.st_size;
return num;
}
而后我们编写c汇编函数OldstatAsm(),这里与之前的Oldlstat()功能基本一致,只是修改了相应函数的系统调用号。
int OldstatAsm()
{
struct stat buf;
int num;
asm volatile(
"mov $0x54, %%eax\n\t"
"int $0x80\n\t"
"mov %%eax, %0\n\t"
: "=m"(buf)
);
num = buf.st_size;
return num;
}
ok,使用gcc编译后我们来看看输出结果:
4.使用gdb分析代码运行
我们在调试中对Oldlstat()和OldlstatAsm()函数的入口与出口插入了断点,用来分析堆栈的变化过程。
这里我们可以看到当main()函数使用Oldlstat()函数,寄存器的位置发生了变化,并保存了入口现场,待调用结束后返回。
这里对于Oldlstat()函数调用结束,此时用来存放buf.st_size结果的变量num已经获得了值,函数位置回到了main()函数并继续执行。
这里主要针对OldlstatAsm的C汇编代码,我们也可以清晰地看到,函数的位置发生了变化,完成切换->执行->返回现场的三步变化。我们使用int 0x80触发中断,然后中断处理程序保存现场,我们的进程陷入内核态。系统调用的工作机制是:当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数,由API、中断向量和中断处理程序协调完成。
三、总结与补充
系统调用的方式: Linux下主要有三种系统调用方式:int 0x80 ,sysenter 和 syscall。传统系统调用(int 0x80) 通过中断/异常实现,在执行 int 指令时,发生 trap。硬件找到在中断描述符表中的表项,在自动切换到内核栈 (tss.ss0 : tss.esp0) 后根据中断描述符的 segment selector 在 GDT / LDT 中找到对应的段描述符,从段描述符拿到段的基址,加载到 cs ,将 offset 加载到 eip。最后硬件将 ss / sp / eflags / cs / ip / error code 依次压到内核栈。返回时,iret 将先前压栈的 ss / sp / eflags / cs / ip 弹出,恢复用户态调用时的寄存器上下文。我们的实验内容主要采取的就是这种方式。
那么为什么需要系统调用呢? linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于内核态,而普通的函数调用由函数库或用户自己提供,运行于用户态。一般的进程是不能访问内核的。它不能访问内核所占内存空间也不能调用内核函数。为了和用户空间上运行的进程进行交互,内核提供了一组接口。透过该接口,应用程序可以访问硬件设备和其他操作系统资源。这组接口在应用程序和内核之间扮演了使者的角色,应用程序发送各种请求,而内核负责满足这些请求。
总结,系统调用是一项非常重要的功能,通过分割用户态与内核态,一方面实现对内核的保护,另一方面解决了用户的需要。但是手写汇编中断程序还是一件非常危险的事情,要谨慎对待!
参考:
https://www.binss.me/blog/the-analysis-of-linux-system-call/ https://blog.csdn.net/gatieme/article/details/50779184