一个操作系统的实现(5)


操作系统内核的雏形

在Linux下用汇编写hello,world

代码及分析:

; 编译链接方法
; (ld 的‘-s’选项意为“strip all”)
; $ nasm -f elf hello.asm -o hello.o
; $ ld -s hello.o -o hello
; $ ./hello
; Hello, world!
; $
[section .data] ; 数据在此
strHello db "Hello, world!", 0Ah
STRLEN equ $ - strHello
[section .text] ; 代码在此
global _start ; 我们必须导出 _start 这个入口,以便让链接器识别
_start:
	mov edx, STRLEN
	mov ecx, strHello
	mov ebx, 1
	mov eax, 4 ; sys_write
	int 0x80 ; 系统调用
	mov ebx, 0
	mov eax, 1 ; sys_exit
	int 0x80 ; 系统调用

程序中定义了两个节(Section),一个放数据,一个放代码。在代码中值得注意的一点是,入口点默认的是“_start”,我们不但要定义它,而且要通过global这个关键字将它导出,这样链接程序才能找到它。至于代码本身,它们是两个系统调用,用来显示字符串并退出。

调试结果:

汇编和C同时使用

关键代码及分析:

**foo.asm

extern choose ; int choose(int a, int b);
[section .data] ; 数据在此
num1st dd 3
num2nd dd 4
[section .text] ; 代码在此
global _start ; 我们必须导出 _start 这个入口,以便让链接器识别。
global myprint ; 导出这个函数为了让 bar.c 使用
_start:
	push dword [num2nd] ; `.
	push dword [num1st] ;  |
	call choose ;  | choose(num1st, num2nd);
	add esp, 8 ; /
	mov ebx, 0
	mov eax, 1 ; sys_exit
	int 0x80 ; 系统调用
	; void myprint(char* msg, int len)
myprint:
	mov edx, [esp + 8] ; len
	mov ecx, [esp + 4] ; msg
	mov ebx, 1
	mov eax, 4 ; sys_write
	int 0x80 ; 系统调用
	Ret

**bar.c

void myprint(char* msg, int len);
int choose(int a, int b){
	if(a >= b){
		myprint("the 1st one\n", 13);
	}
	else{
		myprint("the 2nd one\n", 13);
	}
	return 0;
}

源代码包含两个文件:foo.asm和bar.c。程序入口_start在foo.asm中,一开始程序将会调用bar.c中的函数choose(),choose()将会比较传入的两个参数,根据比较结果的不同打印出不同的字符串。打印字符串的工作是由foo.asm中的函数myprint()来完成的

运行结果:

从Loader到Kernel内核

用Loader加载ELF:

ELF文件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Sections)和节头表(Section header table)。实际上,一个文件中不一定包含全部这些内容,而且它们的位置也未必如图5.2所示这样安排,只有BLF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。

ELF文件结构:

代码分析:

执行命令运行:

nasm boot.asm -o boot.bin
nasm loader.asm -o loader.bin
dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc
sudo mount -o loop a.img /mnt/floppy/
sudo cp loader.bin /mnt/floppy/ -v
sudo umount /mnt/floppy

结果:

跳入保护模式,列出内存情况

代码及分析:  

    ;下面跳入保护模式
    ;加载GDTR
    lgdt    [GdtPtr]
    ;关中断
    cli
    ;打开地址线
    in  al, 92h
    or  al, 00000010b
    out 92h, al
    ;准备切换到保护模式
    mov eax, cr0
    or  eax, 1
    mov cr0, eax
    ;真正进入保护模式
    jmp dword   SelectorFlatC:(BaseOfLoaderPhyAddr+LABEL_PM_START)
    jmp $
wRootDirSizeForLoop dw  RootDirSectors  ; Root Directory 占用的扇区数,
                        ; 在循环中会递减至零.
wSectorNo       dw  0       ; 要读取的扇区号
bOdd            db  0       ; 奇数还是偶数
dwKernelSize        dd  0       ;kernel.bin文件大小
;字符串
KernelFileName      db  "KERNEL  BIN", 0 ; kernel.BIN 之文件名
; 为简化代码, 下面每个字符串的长度均为 MessageLength
MessageLength       equ 9
LoadMessage:        db  "loading  " ; 9字节, 不够则用空格补齐. 序号 0
Message1        db  "ready... " ;  
Message2        db  "No KERNEL" ; 9字节, 不够则用空格补齐. 序号 2
; 从此以后的代码在保护模式下执行 ----------------------------------------------------
; 32 位代码段. 由实模式跳入 ---------------------------------------------------------
[SECTION .s32]
ALIGN   32
[BITS   32]
LABEL_PM_START:
    mov ax, SelectorVideo
    mov gs, ax
    mov ax, SelectorFlatRW
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov ss, ax
    mov esp, TopOfStack
    push    szMemChkTitle
    call    DispStr
    add esp, 4
    call    DispMemInfo
    call    SetupPaging
    mov ah, 0Fh             ; 0000: 黑底    1111: 白字
    mov al, 'P'
    mov [gs:((80 * 0 + 39) * 2)], ax    ; 屏幕第 0 行, 第 39 列。
    jmp $
%include    "lib.inc"
; 显示内存信息 --------------------------------------------------------------
DispMemInfo:
    push    esi
    push    edi
    push    ecx
    mov esi, MemChkBuf
    mov ecx, [dwMCRNumber];for(int i=0;i<[MCRNumber];i++)//每次得到一个ARDS
.loop:                ;{
    mov edx, 5        ;  for(int j=0;j<5;j++)//每次得到一个ARDS中的成员
    mov edi, ARDStruct    ;  {//依次显示:BaseAddrLow,BaseAddrHigh,LengthLow
.1:               ;               LengthHigh,Type
    push    dword [esi]   ;
    call    DispInt       ;    DispInt(MemChkBuf[j*4]); // 显示一个成员
    pop eax       ;
    stosd             ;    ARDStruct[j*4] = MemChkBuf[j*4];
    add esi, 4        ;
    dec edx       ;
    cmp edx, 0        ;
    jnz .1        ;  }
    call    DispReturn    ;  printf("\n");
    cmp dword [dwType], 1 ;  if(Type == AddressRangeMemory)
    jne .2        ;  {
    mov eax, [dwBaseAddrLow];
    add eax, [dwLengthLow];
    cmp eax, [dwMemSize]  ;    if(BaseAddrLow + LengthLow > MemSize)
    jb  .2        ;
    mov [dwMemSize], eax  ;    MemSize = BaseAddrLow + LengthLow;
.2:               ;  }
    loop    .loop         ;}
    call    DispReturn    ;printf("\n");
    push    szRAMSize     ;
    call    DispStr       ;printf("RAM size:");
    add esp, 4        ;
    push    dword [dwMemSize] ;
    call    DispInt       ;DispInt(MemSize);
    add esp, 4        ;
    pop ecx
    pop edi
    pop esi
Ret

分析:

编译链接运行:

nasm boot.asm -o boot.bin
nasm loader.asm -o loader.bin
dd if=boot.bin of=a.img bs=512 count=1 conv=notrunc
sudo mount -o loop a.img /mnt/floppy/
sudo cp loader.bin /mnt/floppy/ -v
sudo umount /mnt/floppy

结果:

将控制权交给内核

关键代码及分析:

; InitKernel
; 将 KERNEL.BIN 的内容经过整理对齐后放到新的位置
InitKernel:
	xor esi,esi
	mov cx, word [BaseOfKernelFilePhyAddr + 2ch] ;program Header Table 中条目(program  header)的个数送cx
	movzx ecx,cx        ;无符号扩展
	mov esi,[BaseOfKernelFilePhyAddr + 1ch] ;program header table 在文件中的偏移量送esi
	add esi,BaseOfKernelFilePhyAddr   ;此时,esi中为program header table 的物理地址
.Begin:
	mov eax,[esi + 0]  ;将program header 描述的段的类型送eax(program header table 的条目就是program  header,program  header的第一个字节是段的类型 )
	cmp eax,0    ;判断类型是否为空,如果是则表示该program  header所描述的段,是不执行的,
	jz .NoAction
	;memcpy(段要加载到的虚拟地址,段的地址,段在文件中的长度) 将文件中的段复制到指定虚拟地址(在program header中有描述)处
	push dword [esi +010h] ;段的长度
	mov eax,[esi + 04h]  ;段在文件中的偏移
	add eax,BaseOfKernelFilePhyAddr    ;段在文件中的偏移+文件的地址=段的地址
	push eax
	push dword [esi + 08h]    ;段要加载到的虚拟地址
	call MemCpy
	add esp,12
.NoAction:
	add esi,20h   ;esi指向下一个program  header
	dec ecx       ;ecx-1 ,表已读完了一个program  header,所以要读的program  header数目减1
	jnz   .Begin    ;program  header 还没读完,则循环读下一个
	Ret

运行结果:


文章作者: Aiaa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Aiaa !
  目录