学期实践

环境配置

前言
博客教程
环境搭建
ubantu64操作系统
csdn
哈工大李治军老师视频教程
蓝桥云课

第零章 常用汇编指令

cmp

cmp是一条汇编指令,用于比较两个操作数的值。它通常与条件跳转指令结合使用,以根据比较结果执行不同的操作。在x86架构中,cmp指令通常用于比较寄存器或内存中存储的值。

以下是cmp指令的一般语法:

cmp operand1, operand2

其中,operand1operand2可以是寄存器、内存地址或立即数。指令的作用是将operand1的值减去operand2的值,并根据结果设置标志寄存器的相应位。具体来说,cmp指令会进行减法运算,但不会保存结果,只更新标志寄存器。

常用的条件跳转指令(如jejnejljg等)可以根据cmp指令设置的标志位来执行相应的条件跳转操作。

例如,以下示例:

mov ax, 10
cmp ax, 5
je equal

在这个示例中,cmp ax, 5将比较ax寄存器中的值和立即数5。如果它们相等,那么je equal将执行跳转到equal标签处的代码。

jz

jz是汇编语言中的一种条件转移指令,它的作用是在”零标志”(ZF)被设置为1时执行跳转。在x86汇编语言中,”零标志”表示最近的运算结果为零。因此,jz指令通常用于检查上一次操作是否导致结果为零,以决定是否执行跳转。

jz指令的全称是”jump if zero”,其语法格式为:

jz target

其中,target是跳转的目标地址。

当执行jz指令时,处理器会检查标志寄存器中的”零标志”是否为1(也就是ZF位)。如果ZF位为1,则跳转到指定的target地址;如果ZF位为0,则继续执行下一条指令。

通常,jz指令与cmp指令一起使用,以根据比较结果决定是否执行跳转操作,例如:

cmp ax, bx
jz equal

在这个示例中,如果axbx的值相等(导致ZF被设置为1),则程序将跳转到equal标签处执行相应的代码。

总之,jz指令是根据”零标志”的状态来进行条件跳转的一种汇编指令。

基本汇编指令

  1. MOV - 数据传送指令

    • 语法: MOV destination, source
    • 功能: 将源操作数的数据传送到目的操作数。
    • 示例:
      MOV AX, BX  ; 将BX寄存器的值传送到AX寄存器
      MOV AL, 34h ; 将立即数34h传送到AL寄存器
      MOV [1234h], AX ; 将AX寄存器的值传送到内存地址1234h
  2. ADD - 加法指令

    • 语法: ADD destination, source
    • 功能: 将源操作数的数据加到目的操作数上,结果存储在目的操作数中。
    • 示例:
      ADD AX, BX  ; 将BX寄存器的值加到AX寄存器上
      ADD AL, 1   ; 将立即数1加到AL寄存器上
      ADD [1234h], AX ; 将AX寄存器的值加到内存地址1234h的值上
  3. SUB - 减法指令

    • 语法: SUB destination, source
    • 功能: 将源操作数的数据从目的操作数中减去,结果存储在目的操作数中。
    • 示例:
      SUB AX, BX  ; 将BX寄存器的值从AX寄存器中减去
      SUB AL, 1   ; 将立即数1从AL寄存器中减去
      SUB [1234h], AX ; 将AX寄存器的值从内存地址1234h的值中减去
  4. INC - 自增指令

    • 语法: INC destination
    • 功能: 将目的操作数的值加1。
    • 示例:
      INC AX  ; 将AX寄存器的值加1
      INC [1234h] ; 将内存地址1234h的值加1
  5. DEC - 自减指令

    • 语法: DEC destination
    • 功能: 将目的操作数的值减1。
    • 示例:
      DEC AX  ; 将AX寄存器的值减1
      DEC [1234h] ; 将内存地址1234h的值减1
  6. JMP - 无条件跳转指令

    • 语法: JMP label
    • 功能: 无条件地跳转到指定的标签位置。
    • 示例:
      JMP START ; 跳转到标签START
  7. CMP - 比较指令

    • 语法: CMP destination, source
    • 功能: 比较目的操作数和源操作数,结果影响标志寄存器。
    • 示例:
      CMP AX, BX  ; 比较AX和BX寄存器的值
      CMP AL, 1   ; 比较AL寄存器的值和立即数1
  8. JE/JZ - 条件跳转指令(等于/零)

    • 语法: JE labelJZ label
    • 功能: 如果比较结果为等于(零标志位被设置),则跳转到指定的标签位置。
    • 示例:
      CMP AX, BX
      JE EQUAL_LABEL ; 如果AX等于BX,则跳转到EQUAL_LABEL
  9. JNE/JNZ - 条件跳转指令(不等于/非零)

    • 语法: JNE labelJNZ label
    • 功能: 如果比较结果为不等于(零标志位未被设置),则跳转到指定的标签位置。
    • 示例:
      CMP AX, BX
      JNE NOT_EQUAL_LABEL ; 如果AX不等于BX,则跳转到NOT_EQUAL_LABEL

示例程序

以下是一个简单的汇编程序示例,演示了上述指令的使用:

section .data
    num1 dw 10
    num2 dw 20
    result dw 0

section .text
    global _start

_start:
    ; 将num1的值加载到AX寄存器
    MOV AX, [num1]
    
    ; 将num2的值加载到BX寄存器
    MOV BX, [num2]
    
    ; 比较AX和BX
    CMP AX, BX
    
    ; 如果AX等于BX,跳转到EQUAL_LABEL
    JE EQUAL_LABEL
    
    ; 如果AX不等于BX,跳转到NOT_EQUAL_LABEL
    JNE NOT_EQUAL_LABEL

EQUAL_LABEL:
    ; 如果相等,将AX的值加1
    INC AX
    JMP END

NOT_EQUAL_LABEL:
    ; 如果不相等,将AX的值减1
    DEC AX

END:
    ; 将AX的值存储到result
    MOV [result], AX
    
    ; 退出程序
    MOV AX, 1
    INT 0x80

第一章 打开电源以后发生的事

计算机历史

  • 从白纸到图灵机
    png
  • 从图灵机到通用图灵机
    png
  • 从通用图灵机到计算机
    png

打开电源

png

  • 引导程序(扇区): 存储在0磁道0扇区(一个扇区一般是512字节),操作系统读入的第一段代码,操作系统的故事由此开始。。。
    png

png

png

引导扇区代码bootset.s,读入系统

  • .s表示汇编代码
  • 汇编代码 和 高级语言代码的区别
    汇编代码每一条都变成了机器指令,你能对其进行完整的控制,不想c语言中,int i; 你无法控制i到底最后会出现在内存的哪一个地址`
! Linux0.11的bootsect.s源代码

.globl begtext, begdata, begbss, endtext, enddata, endbss
.text   //文本段
begtext:
.data   //数据段
begdata:
.bss    //未初始化数据段
begbss:
.text

SETUPLEN = 4                ! nr of setup-sectors
BOOTSEG  = 0x07c0           ! original address of boot-sector
INITSEG  = 0x9000           ! we move boot here - out of the way
SETUPSEG = 0x9020           ! setup starts here
SYSSEG   = 0x1000           ! system loaded at 0x10000 (65536).
ENDSEG   = SYSSEG + SYSSIZE     ! where to stop loading

! ROOT_DEV: 0x000 - same type of floppy as boot.
!       0x301 - first partition on first drive etc
ROOT_DEV = 0x306

entry _start    //entry关键字,告诉链接器程序入口
_start:
    mov ax,#BOOTSEG
    mov ds,ax       ! 把0x07c0赋给ax,然后把ax付给ds(段寄存器)
    mov ax,#INITSEG
    mov es,ax       ! es = ax = 0x9000
    mov cx,#256     
    sub si,si       ! si - si自己的值肯定等于0
    sub di,di       ! sub两行代码给出了两个地址值 分别是 ds:si = 0x7c00 和 es:di = 0x90000, 0x7c00就是一开始计算机启动时读入那个bootsect.s程序的起始地址,大小位512Byte!!!

    rep movw        ! 重复移动(w)字(cx的值256个字,512字节)
    jmpi    go,INITSEG  ! jmpi是间接跳转; go(标号,将来编译完后,go的值就是从start开始的地方到go定义的偏移值)赋给ip寄存器,INITSEG(0x9000)赋给CS寄存器,其实相当于移完之后在跳转到改变后的go位置继续顺序执行代码
go: mov ax,cs
    mov ds,ax
    mov es,ax
  1. DS(数据段寄存器)

    • 作用:用来保存内存中存放数据的数据段地址
    • 默认偏移寄存器:在使用BX, SI和DI而省略段寄存器时,默认段寄存器是DS。但具体哪个偏移寄存器与DS结合使用,取决于具体的指令和上下文。不过,通常BX、SI和DI都可以与DS结合使用来寻址。
  2. ES(附加段寄存器)

    • 作用:存放当前执行程序中一个辅助数据段的段地址。这通常用于处理与DS不同的另一个数据段。
    • 默认偏移寄存器:ES通常与DI(数据索引寄存器)结合使用,尤其是在涉及串操作的指令中。如参考文章2所述,默认的与段地址寄存器ES相结合的偏移地址寄存器是DI。

总结起来:

  • DS段寄存器的默认偏移寄存器可以是BX、SI或DI,具体取决于指令和上下文。

  • ES段寄存器的默认偏移寄存器是DI,特别是在串操作指令中。

  • int 0x10中断
    png

png

png

! 打印加载语句的相关代码(同样来自bootsect.s)
! Print some inane message

    mov ah,#0x03        ! read cursor pos ,BIOS中断功能号,读取光标位置  
    xor bh,bh           !BH通常是页面号,设置为0  
    int 0x10            !调用BIOS中断  
    
    mov cx,#32          !使用之前计算的字符串长度
    mov bx,#0x0007      ! page 0, attribute 7 (normal) 设置显示属性(正常文本)
    mov bp,#msg1        !设置字符串的偏移地址
    mov ax,#0x1301      ! write string, move cursor,BIOS中断功能号,写字符串并更新光标位置  
    int 0x10

! ok, we've written the message, now
...

msg1:
    .byte 13,10
    .ascii "Loading Xubin system ... "
    .byte 13,10,13,10   !回车符ascii码值13,换行符10

png

这段汇编代码是一个简单的启动引导扇区实现,通常用于操作系统的引导程序。

  1. entry _start: 这行指定程序的入口点为 _start 标签。

  2. _start:: 定义了一个名为 _start 的标签,标记着程序的起始位置。

  3. mov ah,#0x03: 将常量 0x03 移动到寄存器 ah 中,通常用于设置视频模式。

  4. xor bh,bh: 将寄存器 bh 和自身执行异或运算,相当于将 bh 寄存器清零。

  5. int 0x10: 触发 BIOS 中断 0x10,用于调用视频服务例程。

  6. mov cx,#36: 将常量 36 移动到寄存器 cx 中,通常用于设置字符串的长度。

  7. mov bx,#0x0007: 将常量 0x0007 移动到寄存器 bx 中,通常用于设置文本输出的属性。

  8. mov bp,#msg1: 将 msg1 标签的地址移动到寄存器 bp 中,用于指向消息字符串。

  9. mov ax,#0x07c0: 将常量 0x07c0 移动到寄存器 ax 中。

  10. mov es,ax: 将寄存器 ax 中的值移动到段寄存器 es 中,通常用于设置附加段。

  11. mov ax,#0x1301: 将常量 0x1301 移动到寄存器 ax 中,用于在屏幕上显示字符串。

  12. int 0x10: 再次触发 BIOS 中断 0x10,用于在屏幕上显示消息。

  13. inf_loop:: 定义了一个名为 inf_loop 的标签,用于创建一个无限循环。

  14. jmp inf_loop: 无条件地跳转到 inf_loop 标签处,实现一个无限循环。

  15. msg1:: 定义了一个名为 msg1 的标签,用于存储消息字符串。

  16. .byte 13,10: 表示换行符。

  17. .ascii "Hello OS world, my name is LZJ": 存储要显示的消息字符串。

  18. .byte 13,10,13,10: 包含连续的两个换行符,用于在屏幕上创建空行。

  19. .org 510: 将当前汇编位置设置为内存地址 510,用于设置引导标志。

  20. boot_flag:: 定义了一个名为 boot_flag 的标签,用于存储引导标志。

  21. .word 0xAA55: 将常量 0xAA55 存储在 boot_flag 标签处,用于指示 BIOS 这是一个引导扇区。

这段代码主要实现了在屏幕上显示消息字符串,并设置了引导标志,以便 BIOS 将其识别为引导扇区。最后,通过一个无限循环保持程序执行。

编译与运行

alt text
alt text
alt text
alt text

总结

  • 实模式 与 保护模式
  • 逻辑地址 与 物理地址
    bootsect就是开机时最先被读入内存(0x7c00)的代码,(因为ipcs的值决定取指执行从第0磁道第1个扇区开始,存放的就是bootsect.s,然后bootsect.s内部把自己转移到了0x900000x90200,并接着从0x90200开始载入了setup模块,打印开机提示”Loading System …”,最后把控制权交给了setup代码段(通过jmpi 0 0x9020CS:IP = 0x9020:0,逻辑地址转换成物理地址就是0x90200

setup.s操作系统接管硬件,获取计算机信息

png

  • 初始化gdt(全局描述符表)
    png

  • 启动保护模式
    png

保护模式下的地址翻译

png

  • jmp 0 8指令的作用
    alt text

makefile文件

操作系统最后编译出来叫做Image镜像,

makefile是一种树状结构

system模块

alt text

alt text

alt text

alt text

alt text

实验1

  1. 修改bootsect.s的代码,输出”Hello XubinOS, loading”
    alt text
  2. bootsect.s 能完成 setup.s 的载入,并跳转到 setup.s 开始地址执行,在
    setup.s 开始执行时需要向屏幕输出一行”Now we are in SETUP” 信息,表示我们进入了 setup 部分。
    ah,#0x03
    mov ah,#0x03 xor bh,bh int 0x10 mov cx,#25 mov bx,#0x0007 mov bp,#msg2 mov ax,cs mov es,ax mov ax,#0x1301 int 0x10 !死循环代码 inf_loop: jmp inf_loop msg2: .byte 13,10 .ascii "Now we are in SETUP!" .byte 13,10,13,10 .org 510 boot_flag: .word 0xAA55
    alt text
    alt text

操作系统接口

什么是接口?

alt text

alt text

系统调用是怎么实现的

alt text

alt text

alt text

应用程序如何系统调用?

在通常情况下,调用系统调用和调用一个普通的自定义函数在代码上并没有什么区别,但调用后发生的事情有很大不同。

调用自定义函数是通过 call 指令直接跳转到该函数的地址,继续运行。

而调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫 API(Application Programming Interface)。API 并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:

把系统调用的编号存入 EAX;
把函数参数存入其它通用寄存器;
触发 0x80 号中断(int 0x80)。

我们不妨看看 lib/close.c,研究一下 close() 的 API:

#define __LIBRARY__
#include <unistd.h>

_syscall1(int, close, int, fd)

其中 _syscall1 是一个宏,在 include/unistd.h 中定义。

#define _syscall1(type,name,atype,a) \
type name(atype a) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
    : "=a" (__res) \
    : "0" (__NR_##name),"b" ((long)(a))); \
if (__res >= 0) \
    return (type) __res; \
errno = -__res; \
return -1; \
}

将 _syscall1(int,close,int,fd) 进行宏展开,可以得到:

int close(int fd)
{
    long __res;
    __asm__ volatile ("int $0x80"
        : "=a" (__res)
        : "0" (__NR_close),"b" ((long)(fd)));
    if (__res >= 0)
        return (int) __res;
    errno = -__res;
    return -1;
}

这就是 API 的定义。它先将宏 __NR_close 存入 EAX,将参数 fd 存入 EBX,然后进行 0x80 中断调用。调用返回后,从 EAX 取出返回值,存入 __res,再通过对 __res 的判断决定传给 API 的调用者什么样的返回值。

其中 __NR_close 就是系统调用的编号,在 include/unistd.h 中定义:

#define __NR_close    6
/*
所以添加系统调用时需要修改include/unistd.h文件,
使其包含__NR_whoami和__NR_iam。
*/
/*
而在应用程序中,要有:
*/

/* 有它,_syscall1 等才有效。详见unistd.h */
#define __LIBRARY__

/* 有它,编译器才能获知自定义的系统调用的编号 */
#include "unistd.h"

/* iam()在用户空间的接口函数 */
_syscall1(int, iam, const char*, name);

/* whoami()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size);

在 0.11 环境下编译 C 程序,包含的头文件都在 /usr/include 目录下。

该目录下的 unistd.h 是标准头文件(它和 0.11 源码树中的 unistd.h 并不是同一个文件,虽然内容可能相同),没有 __NR_whoami__NR_iam 两个宏,需要手工加上它们,也可以直接从修改过的 0.11 源码树中拷贝新的 unistd.h 过来。

alt text

alt text

从“int 0x80”进入内核函数

alt text

alt text

int 0x80 触发后,接下来就是内核的中断处理了。先了解一下 Linux0.11 处理 0x80 号中断的过程。

在内核初始化时,主函数(在 init/main.c 中,Linux 实验环境下是 main(),Windows 下因编译器兼容性问题被换名为 start())调用了 sched_init() 初始化函数:

void main(void)
{
//    ……
    time_init();
    sched_init();
    buffer_init(buffer_memory_end);
//    ……
}

sched_init()kernel/sched.c 中定义为:

void sched_init(void)
{
//    ……
    set_system_gate(0x80,&system_call);
}

set_system_gate 是个宏,在 include/asm/system.h 中定义为:

#define set_system_gate(n,addr) \
    _set_gate(&idt[n],15,3,addr)

_set_gate 的定义是:

#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
    "movw %0,%%dx\n\t" \
    "movl %%eax,%1\n\t" \
    "movl %%edx,%2" \
    : \
    : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
    "o" (*((char *) (gate_addr))), \
    "o" (*(4+(char *) (gate_addr))), \
    "d" ((char *) (addr)),"a" (0x00080000))

虽然看起来挺麻烦,但实际上很简单,就是填写 IDT(中断描述符表),将 system_call 函数地址写到 0x80 对应的中断描述符中,也就是在中断 0x80 发生后,自动调用函数 system_call。具体细节请参考《注释》的第 4 章。

alt text

接下来看 system_call。该函数纯汇编打造,定义在 kernel/system_call.s 中:

!……
! # 这是系统调用总数。如果增删了系统调用,必须做相应修改
nr_system_calls = 72
!……

.globl system_call
.align 2
system_call:

! # 检查系统调用编号是否在合法范围内
    cmpl \$nr_system_calls-1,%eax
    ja bad_sys_call
    push %ds
    push %es
    push %fs
    pushl %edx
    pushl %ecx

! # push %ebx,%ecx,%edx,是传递给系统调用的参数
    pushl %ebx

! # 让ds, es指向GDT,内核地址空间
    movl $0x10,%edx
    mov %dx,%ds
    mov %dx,%es
    movl $0x17,%edx
! # 让fs指向LDT,用户地址空间
    mov %dx,%fs
    call sys_call_table(,%eax,4)
    pushl %eax
    movl current,%eax
    cmpl $0,state(%eax)
    jne reschedule
    cmpl $0,counter(%eax)
    je reschedule

system_call 用 .globl 修饰为其他函数可见。

Windows 实验环境下会看到它有一个下划线前缀,这是不同版本编译器的特质决定的,没有实质区别。

call sys_call_table(,%eax,4) 之前是一些压栈保护,修改段选择子为内核段,call sys_call_table(,%eax,4) 之后是看看是否需要重新调度,这些都与本实验没有直接关系,此处只关心 call sys_call_table(,%eax,4) 这一句。

根据汇编寻址方法它实际上是:call sys_call_table + 4 * %eax,其中 eax 中放的是系统调用号,即 __NR_xxxxxx

显然,sys_call_table 一定是一个函数指针数组的起始地址,它定义在 include/linux/sys.h 中:

fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,...}

增加实验要求的系统调用,需要在这个函数表中增加两个函数引用 ——sys_iamsys_whoami。当然该函数在 sys_call_table 数组中的位置必须和 __NR_xxxxxx 的值对应上。

同时还要仿照此文件中前面各个系统调用的写法,加上:

extern int sys_whoami();
extern int sys_iam();

不然,编译会出错的。

实现 sys_iam() 和 sys_whoami()

添加系统调用的最后一步,是在内核中实现函数 sys_iam()sys_whoami()

每个系统调用都有一个 sys_xxxxxx() 与之对应,它们都是我们学习和模仿的好对象。

比如在 fs/open.c 中的 sys_close(int fd):

int sys_close(unsigned int fd)
{
//    ……
    return (0);
}

它没有什么特别的,都是实实在在地做 close() 该做的事情。

所以只要自己创建一个文件:kernel/who.c,然后实现两个函数就万事大吉了。

修改makefile文件

要想让我们添加的kernel/who.c可以和其它 Linux 代码编译链接到一起,必须要修改 Makefile 文件。

Makefile 里记录的是所有源程序文件的编译、链接规则,《注释》3.6 节有简略介绍。我们之所以简单地运行 make 就可以编译整个代码树,是因为 make 完全按照 Makefile 里的指示工作。

如果想要深入学习 Makefile,可以选择实验楼的课程: 《Makefile 基础教程》、《跟我一起来玩转 Makefile》。

Makefile 在代码树中有很多,分别负责不同模块的编译工作。我们要修改的是 kernel/Makefile。需要修改两处。

(1)第一处

OBJS  = sched.o system_call.o traps.o asm.o fork.o \
        panic.o printk.o vsprintf.o sys.o exit.o \
        signal.o mktime.o

改为:

OBJS  = sched.o system_call.o traps.o asm.o fork.o \
        panic.o printk.o vsprintf.o sys.o exit.o \
        signal.o mktime.o who.o

添加了 who.o。

(2)第二处

### Dependencies:
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
  ../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
  ../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
  ../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
  ../include/asm/segment.h

改为:

### Dependencies:
who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
  ../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \
  ../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \
  ../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \
  ../include/asm/segment.h

添加了 who.sprintf() who.o: who.c ../include/linux/kernel.h ../include/unistd.h

Makefile 修改后,和往常一样 make all 就能自动把 who.c 加入到内核中了。

如果编译时提示 who.c 有错误,就说明修改生效了。所以,有意或无意地制造一两个错误也不完全是坏事,至少能证明 Makefile 是对的。

printk()调试内核

oslab 实验环境提供了基于 C 语言和汇编语言的两种调试手段。除此之外,适当地向屏幕输出一些程序运行状态的信息,也是一种很高效、便捷的调试方法,有时甚至是唯一的方法,被称为“printf 法”。

要知道到,printf() 是一个只能在用户模式下执行的函数,而系统调用是在内核模式中运行,所以 printf() 不可用,要用 printk()

printk()printf() 的接口和功能基本相同,只是代码上有一点点不同。printk() 需要特别处理一下 fs 寄存器,它是专用于用户模式的段寄存器。

看一看 printk 的代码(在 kernel/printk.c 中)就知道了:

int printk(const char *fmt, ...)
{
//    ……
    __asm__("push %%fs\n\t"
            "push %%ds\n\t"
            "pop %%fs\n\t"
            "pushl %0\n\t"
            "pushl $buf\n\t"
            "pushl $0\n\t"
            "call tty_write\n\t"
            "addl $8,%%esp\n\t"
            "popl %0\n\t"
            "pop %%fs"
            ::"r" (i):"ax","cx","dx");
//    ……
}

显然,printk() 首先 push %fs 保存这个指向用户段的寄存器,在最后 pop %fs 将其恢复,printk() 的核心仍然是调用 tty_write()。查看 printf() 可以看到,它最终也要落实到这个函数上。

编写测试程序

激动地运行一下由你亲手修改过的 “Linux 0.11 pro++”!然后编写一个简单的应用程序进行测试。

比如在 sys_iam() 中向终端 printk() 一些信息,让应用程序调用 iam(),从结果可以看出系统调用是否被真的调用到了。

可以直接在 Linux 0.11 环境下用 vi 编写(别忘了经常执行“sync”以确保内存缓冲区的数据写入磁盘),也可以在 Ubuntu 或 Windows 下编完后再传到 Linux 0.11 下。无论如何,最终都必须在 Linux 0.11 下编译。编译命令是:

$ gcc -o iam iam.c -Wall

gcc 的 “-Wall” 参数是给出所有的编译警告信息,“-o” 参数指定生成的执行文件名是 iam,用下面命令运行它:

$ ./iam

如果如愿输出了你的信息,就说明你添加的系统调用生效了。否则,就还要继续调试,祝你好运!

实验

题目:
此次实践项目的基本内容是添加两个系统调用,并编写简单的应用程序进
行测试。
第一个系统调用是iam,其函数原型为:int iam(const char* name);完成的功能是将字符串参数name的内容拷贝到内核中并保存下来。要求name的长度不能超过23个字符,返回值是拷贝的字符数。
如果name的字符个数超过了23,则返回“-1”,并置errno为EINVAL。

第二个系统调用是whoami,其函数原型为:int whoami(char*name,unsigned int size); 该系统调用将内核中由iam()保存的名字拷贝到name指向的用户地址
空间中,同时确保不会对name越界访存(name的大小由size说明)。
返回值是拷贝的字符数。如果size小于需要的空间,则返回“-1”,并置errno为EINVAL

测试程序是编写两个用户态测试程序iam.c和whoami.c,其中iam.c要通过
系统调用iam设置内核中的一个字符串,而whoami.c则通过系统调用是whoami
来取出这个内核字符串,并用printf在屏幕上输出。最终的运行结果是:

[usr/root]# ./iam 
lizhijun
[usr/root]# ./whoami
lizhijun
  • 具体步骤
  1. 先在oslab目录下->sudo ./mount-hdc,挂载Bochs虚拟机
  2. 在~/oslab/hdc/usr/root下书写 iam.c,whoami.c
  3. 修改linux0.11/kernel/syscall.s中的系统调用总数为74(原本是72)
  4. linux-0.11/include/linux/sys.h 修改调用表,添加上你要增加的系统调用
    …..
    blog详细教程

alt text

总结

alt text