linux0.11 阅读笔记

----------------------------------------
linux0.11 阅读笔记
writer: hjjdebug
date: 2019年 07月 30日 星期二 10:20:44 CST
----------------------------------------
代码从以下网址获取:

https://github.com/voidccc/linux0.11

或者

https://github.com/hjjdebug/linux0.11

目前的cpu 已经进展到64bit, 老内核是32位程序,所以在64位机器上编译出32位程序,要做如下调整
请对应修改Makefile
as --32
gcc -m32
ld -m elf_i386
我的cpu 64bits, 系统ubuntu14.04, gcc version: 4.8.4

甲: 理解x32 汇编
1. 内嵌汇编语言检查
读懂strstr() 和 strtok(),
只所以如此要求,是因为不调试只靠读代码甚至是带注释的代码也看不懂了.
因为注释毕竟是有限的,前因后果没法描述清楚,也是对注释的一个挑战!
而且调试会让你知道的更多. 在你读懂的时候,必须掌握一些技巧.
例如想近距离观察一下内嵌汇编到底怎样被编译成了汇编,
用下面命令生成反汇编文件,比直接在gdb中看舒服!
objdump -S -d string.o >mystring.s
注意,反汇编obj文件有地址重定位问题.注意鉴别!

AT&T语法,base(offset, index, i),就是 *(base+offset+index*i)
0x4(%eax,%edx,4) => *(4+%eax+(%edx * 4)), 里边是计算地址的公式,取该地址处内容.
例如:
    mov    0x4(%eax,%edx,4),%eax
    将某地址处内容送eax.

2. 内嵌汇编也有出错的时候,例如
    getbase 函数,在-g3 不优化时,dh,dl 不能获得期望的输出(被edx覆盖),跟踪调试后改为ch,cl 正确

3. 80386汇编也确实有点刁,还要设置gdt及各个段描述符等才能让cpu工作, 进入用户模式前还要设置ldt,tr等段及描述符。
    启用了mm还要设置内存映射表,好在用户程序不需要管这些破烂事了. 这无疑增加了内核代码的阅读难度!
    要认真阅读linux0.00代码, 加深对386编程的理解,它是linux0.11的一个阶梯.


乙: 理解进程概念
    进程是一个运行中程序. 进程起码包含所有的寄存器,还包括程序区,数据区,bss区,x86用一个tss段一个ldt段对应一个进程
1. 进程的创建
  进程的创建是通过fork来创建的,
    先找到一个空的任务槽,find_empty_process, 返回是是一个任务号nr(1,2,3...)
    然后再copy_process
    copy_process 栈中已经保存了所有寄存器内容. 足以构建一个进程结构变量
    进程结构变量存储在一个新申请的页中. 其地址(物理地址)放入task[nr];
    copy_mem(nr,p), nr 确定了新的data_base(及code_base)=nr*0x400,0000, 这是线性地址.
    要设置p->ldt[1](cs),p->ldt[2](ds)为新new_code_base,new_data_base. 所以任务1的线性地址都是0x400,0000以上
    设置ldt表使用的是线性地址.
    需要构建新的页目录项,需要申请新的页表,并拷贝旧页表项,其物理映射还是指向同一个代码区和数据区,但设置数据区不可写.
    当对数据区有写操作时, 会引起页写保护错误, 错误处理程序从出错的线性地址找到他的页表入口地址,将这个内存页置为可写,
    若有必要,也可能新申请一页内存, 让这个table_entry的内容指向这个新的页. 从而可以使用这个新申请的页内存.
    在这个表中所填的地址是物理地址,就是出现在memory 地址线上的地址。
    (开启了mm的后的内存访问方式)

    子进程只是copy了父进程页目录和页表项,并没有真正copy代码区和数据区,这需要写时复制和需求加载,将它们滞后处理了。


2.  fork 函数是唯一一个
      无输入参数有两个返回值的函数,返回值用以区分是父进程还是子进程
     fork 是怎样做到输入无参数,输出两个返回值的?
    if(pid=fork())
    { // 父进程
    }
    else
    {//pid=0 子进程
    }
    简单的说是copy_process 函数修改了子进程的TSS(task stauts segment)内容,使它的eax=0. 将来子进程恢复时,eax=0.
    fork() 调用int80 系统中断,返回后如果没有发生调度,将继续执行父进程,把last_pid 付给eax返回.
    若此时发生了进程调度,或者以后发生了进程调度,调度到了子进程,子进程从它的TSS 中恢复上下文
    执行的位置被定义在int 80 之后(已经存储在了TSS1段中)(eax 向pid赋值之处), 但eax 在上下文中被定义为0,故返回0.
    这样就说明了若未发生调度,将返回last_pid, 若调度到了子进程,则返回0的道理.
    那会不会调度到父进程呢? 不调度就是父进程,调度走了父进程就要歇一会了,临走会把自己的环境保存到自己的TSS段中.

3. 从进程0 fork 进程1, 第一次缺页中断是怎么来的
    进程1 fork 进程0,与其共享代码段和数据段,但设置数据段为只读(*table_entry 项中设定),当调度到进程1,向数据段(0x17)写
    数据时会引起写保护异常,异常服务程序会申请新一页内存,将*table_entry 指向本页,并设内存可读写,将进程1的数据内存与
    进程0的数据内存进行了分离.
    虽然进程0,进程1的代码区物理内存是一样的,但线性地址却是不一样的,后者比前者大64M, 但被映射到了同一个物理地址
    在这里,需要理解逻辑地址,线性地址和物理地址的概念.
    简言之,逻辑地址是选择子加地址偏移,编译器看到的地址. 由于选择子往往是不变的,所以更侧重于地址偏移.
    线性地址是cpu地址,加上选择子所指基地址后形成的地址.
    物理地址是线性地址经mmu转化形成的地址,是memory上看到的地址

丙: 驱动;
无驱不动,驱动就是基本的数据输入输出
1. 串行口驱动
    需要写端口初始化程序,中断服务程序等。
    在rs_init 中
    设置了串行口(0x2f8,0x3f8端口基址)波特率为2400.
    设置中断门0x24 -> rs1_interrupt, 因为串口1中断矢量是0x24
    设置中断门0x23 -> rs2_interrupt, 因为串口2中断矢量是0x23
    中断服务程序:
        首先识别中断源,对于线路状态改变和modem状态改变只需要读状态寄存器即可,对于读字符中断(芯片收到了字符)
        读入放到第一缓冲区,再处理字符到第二缓冲区.
        对于写字符,如果写缓冲空了,就关发送空中断请求寄存器
    中间层接口程序(tty_read, tty_write): tty_write->rs_write 会打开发送空中断请求寄存器
        tty_write->con_write, 可以看到并不是简单的把字符向显示缓冲区copy,还要处理控制字符,还要4中工作状态,可以控制终端显示.
        tty_read 从queue 的secondary 中读取数据

2. 键盘驱动
    在con_init 中:
    set_trap_gate(0x21,&keyboard_interrupt);
    中断服务程序:
        虽然大部分健的处理方式是do_self, 把一个或几个字符放入队列(有ctrl,alt,shift 映射),但也有功能健处理,有e0,e1扩展健处理
    tty_read 可以调用到它    
3. 显示器驱动
    虽然不是中断程序,但需要处理好几个变量.
    a. 光标(x,y) 用 pos=origin + y*video_size_row + (x<<1); 变量跟踪光标
         屏幕大小(video_num_columns,video_num_lines)
           起始内存地址origin
    b. 设置origin, 设置光标
        static inline void set_origin(void)
        {
            cli();
            outb_p(12, video_port_reg); //控制寄存器
            outb_p(0xff&((origin-video_mem_start)>>9), video_port_val); //值寄存器
            outb_p(13, video_port_reg);
            outb_p(0xff&((origin-video_mem_start)>>1), video_port_val);
            sti();
        }

        static inline void set_cursor(void)
        {
            cli();
            outb_p(14, video_port_reg);
            outb_p(0xff&((pos-video_mem_start)>>9), video_port_val);
            outb_p(15, video_port_reg);
            outb_p(0xff&((pos-video_mem_start)>>1), video_port_val);
            sti();
        }
    c. 上滚,下滚是通过移动内存来实现的.
4. 硬盘驱动
    在hd_init()中:
    set_intr_gate(0x2E,&hd_interrupt);
    中断服务程序hd_interrupt: 调用do_hd, 这个do_hd 到底是个什么东西?在代码中找没有看到声明.
    nm blk_drv.a  |grep do_hd
    看到 00000000 B do_hd , 在BSS 区
    objdump -t blk_drv.a |grep do_hd
    00000000 g     O .bss    00000004 do_hd
    看到是一个object, 在bss 区,大小为4, 可判断其是一个函数指针变量,可以赋值.

    其实其技巧是(其实不叫技巧,只是增加阅读难度罢了).
    #define DEVICE_INTR do_hd
    void (*DEVICE_INTR)(void) = NULL;
    它用这种方式定义了一个函数指针,与前边看到导出符号在bss相一致.

    继续执行do_hd
    do_hd 可以是read_intr,write_intr
    写操作:
        port_write(HD_DATA,CURRENT->buffer,256);
    读操作:
        port_read(HD_DATA,CURRENT->buffer,256);

    调用硬盘驱动:
    请求项的最大个数是32个。
    硬盘请求项的执行函数是do_hd_request,它负责根据子设备号,将current_request转化为柱面,磁头,扇区
    然后调用hd_out 函数对硬盘发送命令.
    启动写:
        hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr);
    启动读:
        hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr);

5. ROOT_DEV = 0x301 是什么意思?
    它包含主设备0x3, 从设备0x1的含义.
    主设备号将代表块设备的顺序号.
    从设备号代表了数据位置,一个磁盘可以有4个分区.

    struct blk_dev_struct blk_dev[NR_BLK_DEV] = {
        { NULL, NULL },        /* no_dev */
        { NULL, NULL },        /* dev mem */
        { NULL, NULL },        /* dev fd */
        { NULL, NULL },        /* dev hd */
        { NULL, NULL },        /* dev ttyx */
        { NULL, NULL },        /* dev tty */
        { NULL, NULL }        /* dev lp */
    };

    struct blk_dev_struct {
        void (*request_fn)(void);
        struct request * current_request;
    };

    前一项是request_fn, 硬盘就是do_hd_request,
    后一项是当前请求项指针.
    可见主设备号可以找到do_hd_request,
    当然,如果主设备号是2,就会找到do_fd_request 了.
    可见主设备号代表了驱动.
    从设备号,对硬盘而言, 代表了分区号,分区号代表了一块硬盘位置.
    分区位置是从硬盘分区表获取的. 从设备号在do_hd_request 函数中有使用.
    0x300 第一块硬盘   0x305 第二块硬盘
    0x301 第一个分区   0x306 第一个分区
    0x302 第二个分区   0x307 第二个分区
    0x303 第三个分区   0x308 第三个分区
    0x304 第四个分区   0x309 第四个分区

    读到此我知道为什么有人听不懂技术讲座了,因为有的东西是需要有基础的,只有理解的人才能讲清楚.
    听不懂首先是讲的人没有讲清楚,然后是听的人基础不够或没有听清楚.
    初始化,在hd_init() 中
    #define DEVICE_REQUEST do_hd_request
    blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;

高速缓冲区:
    
    extern int end;
    struct buffer_head * start_buffer = (struct buffer_head *) &end;
    这个end 是什么,是 BSS(Block Started by Symbol segment)段的结尾地址,直接拿来使用的.
    00023fb0 B end
     buffer_init 就是让buffer_head * 去指向1K 大小的缓冲块。
     将来查找空缓冲块,就靠freelist 了, 当然,你需要维护好这个自由节点列表.

    硬盘包活软盘的数据都会存到这个缓冲块中.

    bh=find_buffer(dev,block);
    以dev 和 block 做键值(hash key)來查找.
    其中,bread 函数,会返回一个buffer_head *.
    struct buffer_head * bread(int dev,int block)
    关于 NR_BUFFERS, 它就是nr_buffers, 是混用的, linus 当时还不知道应该用全大写代表常量.

其它:
1. printk 与 printf 有何不同,为什么?
    读完代码是很简单的, printf 从用户区获得数据(段选择子0x17), printf 调用write 函数走文件系统.
    printk 是内核代码,需要从内核区获取数据而不是用户区,所以让fs也指向内核区就可以了.(段选择子0x10),
    printk 直接调用tty_write 不走文件系统.

丁: 文件系统:
1.  mount_root 功能:
    首先,从磁盘读超级块.
    if (!(p=read_super(ROOT_DEV)))
    ROOT_DEV 是一个整形变量,由ORIG_ROOT_DEV 来赋值
     ROOT_DEV = ORIG_ROOT_DEV;
    #define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)
    0x901FC 是bootsect 被0x7c00 处移过来的, 由bootsect 1FC位置2个字节来确定.
    read_super 会把超级块及inode位图,izone位图都读走.
    这两个位图都不会超过8个盘块,分别由超级块结构的8个buffer_head 来指向.
    i=p->s_nzones; // 例如i=62000 个逻辑块, 其对应的比特图块是62000 >> 13 个快,因为1块是1Kbytes,8Kbits
    while (-- i >= 0)
        if (!set_bit(i&8191,p->s_zmap[i>>13]->b_data)) //在这8Kbits数据中判别其对应位是否置位. i&8191 是偏移值.
            free++;

    然后从磁盘读根INODE
    if (!(mi=iget(ROOT_DEV,ROOT_INO)))
    定义了一个表inode_table,表的大小是NR_INODE 32 个, 表是一个环形表,每次需要从表中取到一个empty_inode.
    get_empty_inode();

2. 打开文件 open 的过程,例如:
    open("/dev/tty0",O_RDWR,0)
    open 会通过系统调用调用 sys_open, 返回一个fd.
    每个进程,只允许打开NR_OPEN = 20 个文件, 这样fd都是小于NR_OPEN 的 (fd<NR_OPEN)
    file_table 只包含了 NR_FILE = 64 个文件,说明所有的进程加起来打开文件也不能超过64个,小系统,还可以.
    打开的文件会指向file_table中的一项, current->filp[fd]=f;
    int open_namei(const char * pathname, int flag, int mode, struct m_inode ** res_inode)
    给定路径,返回给定路径所对应的inode
    它调用:
    static struct m_inode * dir_namei(const char * pathname, int * namelen, const char ** name)
    给定路径,返回目录节点和文件名称, 把一件事情分两步来做.
    它调用
    static struct m_inode * get_dir(const char * pathname)
    给定路径,返回最末一级目录节点. 当路径中不再包含'/'时,会退出。这有循环调用的过程
    最初的目录节点从根或者当前目录开始搜索.
    它调用
    static struct buffer_head * find_entry(struct m_inode ** dir, const char * name, int namelen, struct dir_entry ** res_dir)
    给一个目录节点,读出该目录数据,从这些数据中查找输入的名字,返回buffer_head* 及 res_dir
    这是具体的操作,从一个目录中,查找名字. 返回目录项和buffer_head* bh, bh说明了磁盘数据在内存中位置.

3.    write(1,...) 为什么是向控制台写信息?
    int write(int fd, const char * buf, size_t count);
    因为fd=0是open("/dev/tty0",...)返回的,fd=1,fd=2由fd=0复制
    其inode->i_mode是char 设备, 所以调用rw_char,且其dev 是inode->i_zone[0]
    依据主设备号将调用,rw_ttyx, 它再调用 tty_write(minor,buf,count)); 由于minor 为0,所以就是显示器了.
    其tty->write 就i时 con_write
    

戊: 阅读的一些注意事项
a. 函数及变量不符合匈牙利命名法.
    缺点: 名称失掉了某些信息和意义
b. 全部大写,并不代表它是一个常量. 不过可以当成一个常量来看,例如NR_BUFFERS
  
c. 输入参数不做保存而直接操作,
    缺点: 不利于调试,因为当你想看输入参数时,已被破坏
d. 有的地方还可以再优化.
总的来说,代码干练精巧!

阅读代码总线索: main.c

阅读手段:bochs+gdb远程调试(vim做前端)

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值