0 __asm__
参考1:https://www.cnblogs.com/zhenjingcool/p/15925494.html中的嵌入式汇编部分
参考2:https://blog.csdn.net/yt_42370304/article/details/84982864
00 系统调用int 0x80
在保护模式下,内核采用中断的方式实现系统调用,中断向量号为0x80;这是操作系统在用户态访问内核态唯一的途径。
具体哪种系统调用,是由eax中的值决定的,称为功能号,功能号在include/linux/sys.h中定义的
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid,sys_setregid };
这里定义了一个数组,数组中的元素是相关系统调用函数地址,数组下标就是功能号,比如我们调用int 0x80时eax中存放2的话,就是sys_fork系统调用。
系统调用的一般过程:
1、用户空间的某一个函数,比如我们在c程序写的系统应用中需要创建一个进程,我们会调用c语言API的fork()函数
2、c中的fork()函数中会执行汇编命令int 0x80发起系统调用,因为执行了int 0x80,因而从用户态转到内核态。
3、系统调用int 0x80本质上是一个软中断,其对应一个中断处理程序,这个中断处理程序是system_call.s中的_system_call标号。(为什么中断处理程序是system_call.s后面解释)
4、_system_call中做现场保护,然后根据eax中的系统调用号,查找_sys_call_table中对应的系统调用。并调用sys_fork函数。
为什么int 0x80中断处理程序是system_call.s?下面进行解释
在main.c中执行初始化时,其中有一步是sched_init();,这个函数在sched.c中定义的
void sched_init(void)
{
int i;
struct desc_struct * p;
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
p = gdt+2+FIRST_TSS_ENTRY;
for(i=1;i<NR_TASKS;i++) {
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
/* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
ltr(0);
lldt(0);
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt);
outb(inb_p(0x21)&~0x01,0x21);
set_system_gate(0x80,&system_call);
}
最下面一行set_system_gate(0x80,&system_call);,在这里设置了中断号0x80对应的中断处理程序是system_call。
1 源码
/*
* linux/init/main.c
*
* (C) 1991 Linus Torvalds
*/
#define __LIBRARY__
#include <unistd.h>
#include <time.h>
/*
* we need this inline - forking from kernel space will result
* in NO COPY ON WRITE (!!!), until an execve is executed. This
* is no problem, but for the stack. This is handled by not letting
* main() use the stack at all after fork(). Thus, no function
* calls - which means inline code for fork too, as otherwise we
* would use the stack upon exit from 'fork()'.
*
* Actually only pause and fork are needed inline, so that there
* won't be any messing with the stack from main(), but we define
* some others too.
*/
static inline _syscall0(int,fork)
static inline _syscall0(int,pause)
static inline _syscall1(int,setup,void *,BIOS)
static inline _syscall0(int,sync)
#include <linux/tty.h>
#include <linux/sched.h>
#include <linux/head.h>
#include <asm/system.h>
#include <asm/io.h>
#include <stddef.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <linux/fs.h>
static char printbuf[1024];
extern int vsprintf();
extern void init(void);
extern void blk_dev_init(void);
extern void chr_dev_init(void);
extern void hd_init(void);
extern void floppy_init(void);
extern void mem_init(long start, long end);
extern long rd_init(long mem_start, int length);
extern long kernel_mktime(struct tm * tm);
extern long startup_time;
/*
* This is set up by the setup-routine at boot-time
*/
#define EXT_MEM_K (*(unsigned short *)0x90002)
#define DRIVE_INFO (*(struct drive_info *)0x90080)
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)
/*
* Yeah, yeah, it's ugly, but I cannot find how to do this correctly
* and this seems to work. I anybody has more info on the real-time
* clock I'd be interested. Most of this was trial and error, and some
* bios-listing reading. Urghh.
*/
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \
inb_p(0x71); \
})
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
static void time_init(void)
{
struct tm time;
do {
time.tm_sec = CMOS_READ(0);
time.tm_min = CMOS_READ(2);
time.tm_hour = CMOS_READ(4);
time.tm_mday = CMOS_READ(7);
time.tm_mon = CMOS_READ(8);
time.tm_year = CMOS_READ(9);
} while (time.tm_sec != CMOS_READ(0));
BCD_TO_BIN(time.tm_sec);
BCD_TO_BIN(time.tm_min);
BCD_TO_BIN(time.tm_hour);
BCD_TO_BIN(time.tm_mday);
BCD_TO_BIN(time.tm_mon);
BCD_TO_BIN(time.tm_year);
time.tm_mon--;
startup_time = kernel_mktime(&time);
}
static long memory_end = 0;
static long buffer_memory_end = 0;
static long main_memory_start = 0;
struct drive_info { char dummy[32]; } drive_info;
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO;
memory_end = (1<<20) + (EXT_MEM_K<<10);
memory_end &= 0xfffff000;
if (memory_end > 16*1024*1024)
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024)
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024)
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end;
#ifdef RAMDISK
main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
mem_init(main_memory_start,memory_end);
trap_init();
blk_dev_init();
chr_dev_init();
tty_init();
time_init();
sched_init();
buffer_init(buffer_memory_end);
hd_init();
floppy_init();
sti();
move_to_user_mode();
if (!fork()) { /* we count on this going ok */
init();
}
/*
* NOTE!! For any other task 'pause()' would mean we have to get a
* signal to awaken, but task0 is the sole exception (see 'schedule()')
* as task 0 gets activated at every idle moment (when no other tasks
* can run). For task0 'pause()' just means we go check if some other
* task can run, and if not we return here.
*/
for(;;) pause();
}
static int printf(const char *fmt, ...)
{
va_list args;
int i;
va_start(args, fmt);
write(1,printbuf,i=vsprintf(printbuf, fmt, args));
va_end(args);
return i;
}
static char * argv_rc[] = { "/bin/sh", NULL };
static char * envp_rc[] = { "HOME=/", NULL };
static char * argv[] = { "-/bin/sh",NULL };
static char * envp[] = { "HOME=/usr/root", NULL };
void init(void)
{
int pid,i;
setup((void *) &drive_info);
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
NR_BUFFERS*BLOCK_SIZE);
printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
if (!(pid=fork())) {
close(0);
if (open("/etc/rc",O_RDONLY,0))
_exit(1);
execve("/bin/sh",argv_rc,envp_rc);
_exit(2);
}
if (pid>0)
while (pid != wait(&i))
/* nothing */;
while (1) {
if ((pid=fork())<0) {
printf("Fork failed in init\r\n");
continue;
}
if (!pid) {
close(0);close(1);close(2);
setsid();
(void) open("/dev/tty0",O_RDWR,0);
(void) dup(0);
(void) dup(0);
_exit(execve("/bin/sh",argv,envp));
}
while (1)
if (pid == wait(&i))
break;
printf("\n\rchild %d died with code %04x\n\r",pid,i);
sync();
}
_exit(0); /* NOTE! _exit, not exit() */
}
开头
#define __LIBRARY__
#include <unistd.h>
#include <time.h>
接下来,定义了4个内联函数,而且是在unistd.h中的宏定义。
static inline _syscall0(int,fork)
static inline _syscall0(int,pause)
static inline _syscall1(int,setup,void *,BIOS)
static inline _syscall0(int,sync)
我们看一下unistd.h中是如何定义_syscall0的
#define _syscall0(type,name) \
type name(void) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name)); \ //##为C中的语法,这里表示连接__NR_和name,这里是__NR_fork,这个是在本头文件中定义的宏(#define __NR_fork 2)
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
在这个宏定义中,嵌入了汇编代码。
int 0x80:int表示中断,0x80表示系统调用,int 0x80表示发起一个系统调用
"0" (__NR_##name) 为asm的输入,__NR_##name为2,这里意思是把2放入0的位置处,即把2放入eax寄存器中(asm规定,0到9分别表示特定的位置,不明白可以查看:https://blog.csdn.net/yt_42370304/article/details/84982864)
"=a" (__res) 为asm的输出,表示把eax中的内容写到变量__res中。
上面这段代码表示发起一个系统调用,功能号是2,功能号2对应的是fork系统调用。
我们再回过头来看main.c中的这段代码
static inline _syscall0(int,fork)
static inline _syscall0(int,pause)
static inline _syscall1(int,setup,void *,BIOS)
static inline _syscall0(int,sync)
这里定义了4个内联函数,作用分别是系统调用fork、系统调用pause、系统调用setup、系统调用sync。需要注意的是,本文件中定义了这4个系统调用,但是没有在本文件中使用。
下面引入了一些头文件
#include <linux/tty.h>
#include <linux/sched.h>
#include <linux/head.h>
#include <asm/system.h>
#include <asm/io.h>
#include <stddef.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <linux/fs.h>
下面定义了一个char数组
static char printbuf[1024];
下面是一些函数,他们的原型分别在不同的c文件中。
extern int vsprintf();
extern void init(void);
extern void blk_dev_init(void);
extern void chr_dev_init(void);
extern void hd_init(void);
extern void floppy_init(void);
extern void mem_init(long start, long end);
extern long rd_init(long mem_start, int length);
extern long kernel_mktime(struct tm * tm);
extern long startup_time;
我们在setup.s中在0x90000开始的位置放入了一些数据(https://www.cnblogs.com/zhenjingcool/p/15944047.html),这里我们开始使用这些数据
/*
* This is set up by the setup-routine at boot-time
*/
#define EXT_MEM_K (*(unsigned short *)0x90002) //0x90002处存放的是扩展内存大小。这里我们把扩展内存大小赋值给宏EXT_MEM_K
#define DRIVE_INFO (*(struct drive_info *)0x90080) //第一个硬盘的信息
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC) //根设备信息
下面代码是读取cmos实时时钟信息。
#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \ //0x70是写端口号,0x80|addr 是要读取的CMOS 内存地址
inb_p(0x71); \ //0x71 是读端口号。
})
outb_p是一个宏,在io.h中定义,汇编中outb指令是向特定io端口写一字节数据。inb是特定io端口向源头读入1字节数据。
#define outb(value,port) \
__asm__ ("outb %%al,%%dx"::"a" (value),"d" (port)) //无输出,输入:value写入eax,port写入edx。这个嵌入汇编意思是把value发到port端口
#define inb(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al":"=a" (_v):"d" (port)); \ //从port端口读取1字节数据放入eax,并把eax值存入_v;返回_v
_v; \
})
#define outb_p(value,port) \
__asm__ ("outb %%al,%%dx\n" \
"\tjmp 1f\n" \ //jmp 1f意思是跳转到标号1处,方向是向前跳转(因为这里有两个标号1,所以要区分向前还是向后)
"1:\tjmp 1f\n" \
"1:"::"a" (value),"d" (port))
#define inb_p(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al\n" \
"\tjmp 1f\n" \
"1:\tjmp 1f\n" \
"1:":"=a" (_v):"d" (port)); \
_v; \
})
下面代码定义了一个宏,作用是把BCD码转换为二进制的值
#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)
下面代码实现时间初始化。
static void time_init(void)
{
struct tm time; //在time.h中定义的
do {
time.tm_sec = CMOS_READ(0); //读取cmos时间,秒.在教材7.1.3.1小节有相关介绍,cmos在指定位置存放了当前时分秒。通过0x70设置地址,cmos会把读取后的信息放到0x71,。我们读取0x71就获取到值
time.tm_min = CMOS_READ(2); //分钟
time.tm_hour = CMOS_READ(4); //小时
time.tm_mday = CMOS_READ(7); //天
time.tm_mon = CMOS_READ(8); //月
time.tm_year = CMOS_READ(9); //年
} while (time.tm_sec != CMOS_READ(0)); //如果读取时间超过1秒,则重新读取。保证误差在1秒之内
BCD_TO_BIN(time.tm_sec);//cmos中的值是bcd码,这里对bcd码进行转码
BCD_TO_BIN(time.tm_min);
BCD_TO_BIN(time.tm_hour);
BCD_TO_BIN(time.tm_mday);
BCD_TO_BIN(time.tm_mon);
BCD_TO_BIN(time.tm_year);
time.tm_mon--; //因为tm_mon从1-12,这里做减1操作
startup_time = kernel_mktime(&time); //创建时间,赋值给start_time
}
定义了一个结构体
struct drive_info { char dummy[32]; } drive_info;
下面是main函数,在介绍head.s时我们讲过,head.s中设置了调用c中main函数的入口,就是指的这里(注意:汇编调用c中的函数时要加_,即_main)
下面的代码需要对照这个图查看脉络将会更清晰。
系统内存划分为内核程序,缓存,虚拟盘,主存储区这几部分,见下图
void main(void) /* This really IS void, no error here. */
{ /* The startup routine assumes (well, ...) this */
/*
* Interrupts are still disabled. Do necessary setups, then 此时还处于关中断状态,关中断是在setup.s中关闭的,从那以后到目前为止还没有打开。
* enable them
*/
ROOT_DEV = ORIG_ROOT_DEV; //ROOT_DEV是在其他文件中定义的宏,讲到时我们再回来看这地方
drive_info = DRIVE_INFO; //DRIVE_INFO前面定义的宏,这里赋值给driver_info
memory_end = (1<<20) + (EXT_MEM_K<<10); //计算总的内存大小,单位为字节,1左移20位表示1M基本内存地址,EXT_MEM_K是前面定义的宏表示扩展内存大小(kb),这里左移10位转换为字节
memory_end &= 0xfffff000; //因为内存是分页的,1页是4kb,这里把最后不足1页的内存去掉
if (memory_end > 16*1024*1024) //如果内存大于16M,
memory_end = 16*1024*1024;
if (memory_end > 12*1024*1024) //如果内存大于12M,缓冲区大小设置为4M
buffer_memory_end = 4*1024*1024;
else if (memory_end > 6*1024*1024) //如果内存大于6M,缓冲区大小设置为2M
buffer_memory_end = 2*1024*1024;
else
buffer_memory_end = 1*1024*1024;
main_memory_start = buffer_memory_end; //缓冲区后面是主内存的地址
#ifdef RAMDISK //如果定义了虚拟内存,则主内存开始位置还要往后延一点
main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
mem_init(main_memory_start,memory_end); //初始化主内存
trap_init(); //中断门初始化
blk_dev_init(); //块设备初始化
chr_dev_init(); //字符设备初始化
tty_init(); //tty的初始化
time_init(); //时间的初始化
sched_init(); //调度程序的初始化,这里会初始化任务0。因为调度程序依赖于时间片中断,在中断打开之前是不会发生调度的,任务0会等待开中断后由调度程序调度执行?解释:在执行了下面的sti()函数开中断后,接下来将由调度程序决定CPU上下文执行哪个进程,此时只有一个进程:任务0,因此此时任务0开始执行。
buffer_init(buffer_memory_end); //缓冲区管理的初始化
hd_init();//硬盘初始化
floppy_init();//软盘初始化
sti();//打开中断
move_to_user_mode();//在system.h中定义的宏,作用是初始化数据段、附加段。任务0是如何运行的?通过在堆栈中设置的参数,利用中断返回指令启动任务0执行
if (!fork()) { //fork是前面定义的宏,作用是新建子进程,这里创建的是任务1。fork()调用会产生和父进程一样的进程描述符,也就是说父进程和子进程的代码段完全一样,执行相同的代码,对于父进程来说,fork()调用返回进程号pid,对于子进程来说,fork()调用返回0.因此这里if判断条件决定了父进程和子进程不同的代码执行逻辑。此处 子进程中if条件才为真,执行init()函数,父进程不会执行init()函数。
init();//在init中会初始化标准输入stdin、标准输出stdout。然后重新打开一个sh交互程序,也就是我们看到的那个黑框,只要我们不exit,黑框一直存在,也就是这个进程n一直运行,当我们关闭了黑框或者执行exit命令,这个进程n才结束。当然我们可以打开多个交互式sh窗口,也就是打开多个sh进程。
}
/*
* NOTE!! For any other task 'pause()' would mean we have to get a
* signal to awaken, but task0 is the sole exception (see 'schedule()')
* as task 0 gets activated at every idle moment (when no other tasks
* can run). For task0 'pause()' just means we go check if some other
* task can run, and if not we return here.
*/
for(;;) pause(); //这一段的意思是,定义了一个死循环,每循环一次,执行一次pause,如果此时有其他进程在等待,将获取cpu时间片执行;如果没有其他进程,将一直执行for循环;这里就是那个idel进程。运行到这,说明
}
其中下面这些初始化是非常重要的部分,后面博客会一一讲到。
mem_init(main_memory_start,memory_end); //初始化主内存
trap_init(); //中断门初始化
blk_dev_init(); //块设备初始化
chr_dev_init(); //字符设备初始化
tty_init(); //tty的初始化
time_init(); //时间的初始化
sched_init(); //调度程序的初始化,这里会创建任务0,并且移动到任务0中执行
buffer_init(buffer_memory_end); //缓冲区管理的初始化
hd_init();//硬盘初始化
floppy_init();//软盘初始化
下面程序,创建任务1,并执行init函数
if (!fork()) { //fork是前面定义的宏,作用是新建子进程
init();
}
定义了一些字符数组作为sh程序的参数,后面会用到
static char * argv_rc[] = { "/bin/sh", NULL };
static char * envp_rc[] = { "HOME=/", NULL };
static char * argv[] = { "-/bin/sh",NULL };
static char * envp[] = { "HOME=/usr/root", NULL };
我们看一下init()函数。
init函数有如下3个功能:1 安装根文件系统;2 运行系统初始资源配置文件/etc/rc中的命令;3 执行用户登陆shell程序
void init(void)
{
int pid,i;
setup((void *) &drive_info); //设置硬盘,在hd.c中定义
(void) open("/dev/tty0",O_RDWR,0); //以读写的方式打开/dev/tty0。它对应终端控制台。由于这是第一次打开文件操作,因此产生的文件描述符(文件句柄)是0,也就是标准输入句柄stdin
(void) dup(0); //复制文件描述符,产生1号句柄,即stdout标准输出设备
(void) dup(0); //复制文件描述符,产生2号句柄,即stderr标准错误输出设备
printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
NR_BUFFERS*BLOCK_SIZE);
printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
if (!(pid=fork())) { //再创建一个子进程(任务2),同样的道理,对于父进程fork()函数返回pid,对于子进程fork()函数返回0;此处只有子进程(也就是任务2)会进入if分支
close(0);//关闭文件描述符0,并立即打开文件/etc/rc,从而把标准输入stdin定向到/etc/rc文件上。这样所有的标准输入数据都将从该文件中读取。
if (open("/etc/rc",O_RDONLY,0))//打开/etc/rc文件
_exit(1);
execve("/bin/sh",argv_rc,envp_rc);//内核以非交互形式执行/bin/sh,从而实现执行/etc/rc文件中的命令,当该文件中的命令执行完毕后,/bin/sh就会立即退出,因此进程2也就随之结束。
_exit(2); //退出子进程
}
if (pid>0) //如果子进程创建成功,这里是父进程(任务1)执行的分支,只有任务1才能进入if里面
while (pid != wait(&i)) //等待子进程(任务2)结束,任务2以非交互方式运行sh程序,执行/etc/rc里面的命令,运行完则任务2结束
/* nothing */;
while (1) { //如果子进程(任务2)结束,才轮到这里执行
if ((pid=fork())<0) { //再创建一个进程
printf("Fork failed in init\r\n");
continue;
}
if (!pid) { //子进程才会执行这个if分支。在子进程中关闭
close(0);close(1);close(2);//在这个新创建的子进程中,关闭所有以前遗留下来的句柄(stdin,stdout,stderr)
setsid();//新创建一个会话
(void) open("/dev/tty0",O_RDWR,0);//重新打开/dev/tty0作为stdin
(void) dup(0);//stdout
(void) dup(0);//stderr
_exit(execve("/bin/sh",argv,envp));//再次执行/bin/sh程序,但这次执行所传递的参数和前面不一样,参数中包含了-/bin/sh,注意前面的横线-,表示登录shell,和前面非交互式shell不同。这个登录shell将一直运行,除非我们执行exit命令退出。
}
while (1) //这里父进程while循环判断某一子进程是否退出,如果退出则打印"XXX退出"等信息,然后继续等待其他登录shell(子进程)退出。此外wait()函数也处理孤儿进程,把其父进程号改为1号进程。
if (pid == wait(&i))
break;
printf("\n\rchild %d died with code %04x\n\r",pid,i);
sync(); //刷新缓冲区
}
_exit(0); /* NOTE! _exit, not exit() */
}
我们对main.c程序讲解了一个框架,里面一些细节我们会在接下来的文章中逐步深化。