嵌入式汇编+系统调用
init进程调用的init函数
1、setup((void*)&drive_info);
a.setup函数用的是main.c中Line 25的inline _syscall1(int,setup,void *,BIOS),_syscall1()函数调用来自于include/unistd.h中的Line 146
1 | #define _syscall1(type,name,atype,a) \ |
2 | type name(atype a) \ |
3 | { \ |
4 | long __res; \ |
5 | __asm__ volatile ("int $0x80" \ |
6 | : "=a" (__res) \ |
7 | : "0" (__NR_##name),"b" ((long)(a))); \ |
8 | if (__res >= 0) \ |
9 | return (type) __res; \ |
10 | errno = -__res; \ |
11 | return -1; \ |
12 | } |
根据define规则进行替换
1 | int setup(void* BIOS) \ |
2 | { \ |
3 | long __res; \ |
4 | __asm__ volatile ("int $0x80" \ |
5 | : "=a" (__res) \ |
6 | : "0" (__NR_##setup),"b" ((long)(BIOS))); \ |
7 | if (__res >= 0) \ |
8 | return (int) __res; \ |
9 | errno = -__res; \ |
10 | return -1; \ |
11 | } |
b.drive_info
定义:struct drive_info { char dummy[32]; } drive_info;//drive_info结构其实就是个32位的数组而已
#define DRIVE_INFO (*(struct drive_info *)0x90080)//定义DRIVE_INFO为0x90080开始的32位的数据。而在setup.s中已经定义好从0x90080开始存放BIOS两个硬盘的参数表,0x90080处存放第一个硬盘的表,0x90090处存放第二个硬盘的表。(表的长度是16位)
drive_info = DRIVE_INFO;
因此,setup((void*)&drive_info)的作用就是读取硬盘参数包括分区表信息并加载虚拟盘(若存在的话)和安装根文件系统设备。
2、(void) open (“/dev/tty0”,O_RDWR,0)
open函数的定义:
1 | int open(const char * filename, int flag, ...) |
2 | { |
3 | register int res; |
4 | va_list arg; |
5 | |
6 | va_start(arg,flag); |
7 | __asm__("int $0x80" |
8 | :"=a" (res) |
9 | :"0" (__NR_open),"b" (filename),"c" (flag), |
10 | "d" (va_arg(arg,int))); |
11 | if (res>=0) |
12 | return res; |
13 | errno = -res; |
14 | return -1; |
15 | } |
16 |
功能就是打开设备或者文件。多解释一下他的使用吧
int open(const char* pathname,int flags,mode_t mode);
函数说明:
pathname:字符串,指向欲打开的文件或者设备字符串
flags:下列参数flags所能使用的标识
O_RDONLY 以只读方式打开文件
O_WRONLY 以只写方式打开文件
O_RDWR 以可读写方式打开文件。上述三种旗标是互斥的,也就是不可同时使用,但可与下列的旗标利用OR(|)运算符组合。
O_CREAT 若欲打开的文件不存在则自动建立该文件。
O_EXCL 如果O_CREAT 也被设置,此指令会去检查文件是否存在。文件若不存在则建立该文件,否则将导致打开文件错误。此外,若O_CREAT与O_EXCL同时设置,并且欲打开的文件为符号连接,则会打开文件失败。
O_NOCTTY 如果欲打开的文件为终端机设备时,则不会将该终端机当成进程控制终端机。
O_TRUNC 若文件存在并且以可写的方式打开时,此旗标会令文件长度清为0,而原来存于该文件的资料也会消失。
O_APPEND 当读写文件时会从文件尾开始移动,也就是所写入的数据会以附加的方式加入到文件后面。
O_NONBLOCK 以不可阻断的方式打开文件,也就是无论有无数据读取或等待,都会立即返回进程之中。
O_NDELAY 同O_NONBLOCK。
O_SYNC 以同步的方式打开文件。
O_NOFOLLOW 如果参数pathname 所指的文件为一符号连接,则会令打开文件失败。
O_DIRECTORY 如果参数pathname 所指的文件并非为一目录,则会令打开文件失败。
mode_t:仅在创建新文件时使用,用于指定文件的访问权限。因此如果flags没有O_CREAT参数,则第三个参数不起作用,若此时要打开的文件不存在则会报错。
typedef unsigned short mode_t;
使用的规则跟Linux中的一样:
r 4 可读
w 2 可写
x 1 可执行
3、(void) dup(0);
定义:
_syscall1(int,dup,int,fd)展开就可以了int dup(int fd)
由于上一句(void) open(“/dev/tty0”,O_RDWR,0)是第一次打开文件的操作,因此他产生的文件句柄号(文件描述符)肯定是0.然后dup函数的功能就是复制句柄,用于打开不同的标准输出设备。
4、进程2的创建
1 | if (!(pid=fork())) { |
2 | close(0); |
3 | if (open("/etc/rc",O_RDONLY,0)) |
4 | _exit(1); |
5 | execve("/bin/sh",argv_rc,envp_rc); |
6 | _exit(2); |
7 | } |
创建进程2,对于新创建的子进程,fork函数将会返回0值,因此子进程创建成功后,会进入这个if判断中执行。
进程2首先关闭句柄0( (void) open (“/dev/tty0”,O_RDWR,0))。然后以只读的方式打开/etc/rc文件。而对于_exit函数:
volatile void _exit(int exit_code)
{
__asm__("int $0x80"::"a" (__NR_exit),"b" (exit_code));
}
sys_exit是一个只带一个参数exitcode的函数,该参数就是系统退出(sys_exit)的退出码。_exit通过调用0x80号软中断带参数,系统调用号(_NR_exit,即在sys_call_table中的偏移量)和退出码(exitcode)的方法,来达到调用sys_exit的目的。
系统调用sys_exit的处理函数定义在文件kernel/exit.c中。
sys_exit的函数体很简单,只是调用了函数do_exit:
do_exit((error_code&0xff)<<8);
(error_code&0xff)<<8的作用就是将error_code的低8位移到高8位中,低8位用0填补,此数将作为参数传给函数do_exit。
下面来看看函数do_exit是怎样做的。
1 | int do_exit(long code) |
2 | { |
3 | int i; |
4 | |
5 | free_page_tables(get_base(current->ldt[1]),get_limit(0x0f)); |
6 | free_page_tables(get_base(current->ldt[2]),get_limit(0x17)); |
7 | for (i=0 ; i<NR_TASKS ; i++) |
8 | if (task[i] && task[i]->father == current->pid) { |
9 | task[i]->father = 1; |
10 | if (task[i]->state == TASK_ZOMBIE) |
11 | /* assumption task[1] is always init */ |
12 | (void) send_sig(SIGCHLD, task[1], 1); |
13 | } |
14 | for (i=0 ; i<NR_OPEN ; i++) |
15 | if (current->filp[i]) |
16 | sys_close(i); |
17 | iput(current->pwd); |
18 | current->pwd=NULL; |
19 | iput(current->root); |
20 | current->root=NULL; |
21 | iput(current->executable); |
22 | current->executable=NULL; |
23 | if (current->leader && current->tty >= 0) |
24 | tty_table[current->tty].pgrp = 0; |
25 | if (last_task_used_math == current) |
26 | last_task_used_math = NULL; |
27 | if (current->leader) |
28 | kill_session(); |
29 | current->state = TASK_ZOMBIE; |
30 | current->exit_code = code; |
31 | tell_father(current->father); |
32 | schedule(); |
33 | return (-1); /* just to suppress warnings */ |
34 | } |
首先释放当前进程代码段和数据段所占的内存页。函数free_page_tables()的第一个参数(get_base()返回值)指明在CPU线性地址空间中的起始基地址,第2个参数(get_limit()返回值)说明欲释放的字节长度值。get_base()宏中的current->ldt[1]给出进程代码段描述符的位置,current->ldt[2]给出进程数据段描述符的位置。get_limit()中的0x0f是进程代码段的选择符,0x17是进程数据段的选择符。即在取段基地址时使用该段的段描述符所处地址作为参数,取段长度时使用该段的选择符作为参数。
查看free_page_tables.
1 | int free_page_tables(unsigned long from,unsigned long size) |
2 | { |
3 | unsigned long *pg_table; |
4 | unsigned long * dir, nr; |
5 | |
6 | if (from & 0x3fffff) |
7 | panic("free_page_tables called with wrong alignment");//判断线性地址是否在4M的边界上,若不是则显示出错信息,并死机 |
8 | if (!from) |
9 | panic("Trying to free up swapper memory space");//判断指定的地址时候=0,若是则显示出错信息"试图释放内核和缓冲区所占的空间 |
10 | size = (size + 0x3fffff) >> 22;//计算参数size给出的长度所占的页目录项数(因为1个页表管理4M物理内存,所以用右移22位的方式把需要复制的内存长度值除以4M),之所以加上0x3fffff(即4M-1)是为了得到进位整数倍结果,即如果除操作若有余数则size进1,例如,如果原size=4.01M,那么得到的size=2。 |
11 | dir = (unsigned long *) ((from>>20) & 0xffc); //计算给出的线性基地址所对应的起始目录项的地址。因为每个目录项管理4M内存,所以对应的目录项号(表内偏移)=from>>22(from/4M),而每个目录项占4字节,并且由于页目录表从物理地址0开始存放,因此实际目录项指针=目录项号<<2。综上,线性地址所对应的目录项地址就是from>>20。“与”上0xffc确保目录项指针范围有效(屏蔽掉目录指针的最后两位,这样目录指针只能指向4位为单元的目录项的起始处) 此时,size是释放的页表页数;dir是起始目录项的指针 |
12 | for ( ; size-->0 ; dir++) { |
13 | if (!(1 & *dir))//判断dir指向的页表是否可用,实际上是判断P位,参见页目录表项结构图。如果P=0,则表示对应的目录项没有使用,继续处理下一个目录项 |
14 | continue; |
15 | pg_table = (unsigned long *) (0xfffff000 & *dir);如果目录项有效,则取出指向的页表地址 |
16 | for (nr=0 ; nr<1024 ; nr++) { |
17 | if (1 & *pg_table)如果该页表项有效则释放对应页 |
18 | free_page(0xfffff000 & *pg_table); |
19 | *pg_table = 0;页内容清零 |
20 | pg_table++;指向页表中的下一项 |
21 | } |
22 | free_page(0xfffff000 & *dir);释放该页表所占内存页面 |
23 | *dir = 0;对应页表的目录项清零 |
24 | } |
25 | invalidate();刷新页变换高速缓冲 |
26 | return 0; |
27 | } |
其中free_page(unsigned long addr)的作用是释放地址addr开始的一页(4k)内存。
1 | void free_page(unsigned long addr) |
2 | { |
3 | if (addr < LOW_MEM) return;//如果addr小于内存低端1M,则表示在内核程序或高速缓冲中,对此不处理。 |
4 | if (addr >= HIGH_MEMORY)//如果addr大于物理地址最高端,则显示出错信息 |
5 | panic("trying to free nonexistent page"); |
6 | addr -= LOW_MEM; |
7 | addr >>= 12;计算要释放内存的启始页面号=(addr-LOW_MEM)/4096 |
8 | if (mem_map[addr]—) return;//如果该页面号对应的页面映射字节不为0,则减1返回。static unsigned char mem_map [ PAGING_PAGES ] = {0,}; 页面字节映射图,每个页面占用一个字节。为1表示占用,0表示空闲。 |
9 | mem_map[addr]=0;//如果该页面号对应的页面映射字节原本就是0,表示该物理页面本来就是空闲的,显示出错信息。 |
10 | panic("trying to free free page"); |
11 | } |
高速缓冲刷新函数
1 | #define invalidate() \ |
2 | __asm__("movl %%eax,%%cr3"::"a" (0))这里通过重新加载页目录基地址寄存器cr3的方法达到刷新的目的。之所以进行重新加载就可以刷新,是因为重新加载cr3后TLB就会失效,Intel i386已经设定这样就可以实现刷新 |
P位是存在标志。1表示页面有效,0表示无效。