Linux0.00 代码解析(二)

Linux 0.00 的编译、运行、源码下载:
http://blog.csdn.net/longintchar/article/details/78757065
Linux 0.00 Makefile 解读:
http://blog.csdn.net/longintchar/article/details/78857966
Linux 0.00 代码解析——boot.s
http://blog.csdn.net/longintchar/article/details/78766916


Linux-0.00的代码分为两部分——boot.shead.s.

boot.s采用as86语言编写,是引导启动程序,先把内核代码加载到物理地址0x10000处,然后把内核代码移动到物理地址0处,接下来设置临时GDT表等信息,再把处理器设置成保护模式,最后跳转到内核代码处(0地址)运行。

head.s是内核代码,采用GNU as汇编语言编写,实现了2个运行在特权及3上的任务,它们在时钟中断控制下相互切换运行,一个在屏幕上打印“A”,另一个在屏幕上打印“B”。

本文要分析的是head.s。请注意,这段代码在运行的时候,它的起始位置在物理地址0处

1. 设置DS,ES,SS,ESP

SCRN_SEL    = 0x18
TSS0_SEL    = 0x20
LDT0_SEL    = 0x28
TSS1_SEL    = 0X30
LDT1_SEL    = 0x38

SCRN_SEL等都是符号常量,代表某选择子的值,这样写可读性好。相当于c语言中的#define SCRN_SEL 0x18.

.code32
.global startup_32
.text
startup_32:
    movl $0x10,%eax
    mov %ax,%ds

.code32 是我自己加的,不然编译会报错。这句伪指令告诉编译器,下面的代码要编译成32位代码。
.global 表示标识符是外部的或者全局的。
.text 标识正文段的开始,并切换到text段。

movl $0x10,%eax , 0x10是数据段(在boot.s文件中定义)的选择子,此数据段的基地址为0,界限值是0x7FF(10进制2047),粒度4KB;因为粒度是4KB,所以段长度是(2047+1)*4KB=8MB;DPL=0,向上扩展,可读可写。
mov %ax,%ds ,加载ds。

lss init_stack,%esp

init_stack处有6个字节,见

init_stack:       # Will be used as user stack for task0.
    .long init_stack
    .word 0x10

这是一个远指针,前4个字节是偏移,后2个字节是段选择子,这句代码表示用偏移加载esp,用数据段选择子0x10加载ss.

2. setup_idt

此过程用于在IDT(中断描述符表)中安装中断门。代码是

setup_idt:
    lea ignore_int,%edx
    movl $0x00080000,%eax
    movw %dx,%ax        /* selector = 0x0008 = cs */
    movw $0x8E00,%dx   /* interrupt gate - dpl=0, present */
    lea idt,%edi
    mov $256,%ecx
rp_sidt:
    movl %eax,(%edi)
    movl %edx,4(%edi)
    addl $8,%edi
    dec %ecx
    jne rp_sidt
    lidt lidt_opcode
    ret

中断门描述符

中断门描述符如下图:

这里写图片描述

下面是低32位(代码中用eax存储),上面是高32位(代码中用edx存储)。

可以看出,中断门定义了一个长指针(段选择符:过程入口点偏移值),当发生中断的时候,处理器使用这个长指针把程序执行权转移到中断处理过程中。

在edx和eax中组装中断门描述符

lea指令是取有效地址(偏移值)。lea ignore_int,%edx表示把ignore_int处的有效地址传给edx. 注意,是取ignore_int处的偏移地址,而不是ignore_int处存储的内容。这样,过程入口点偏移值31-16组装完毕。
movl $0x00080000,%eax, 段选择符(=0x08,索引1,内核代码段)组装完毕。
movw %dx,%ax, 过程入口点偏移值15-0组装完毕。
movw $0x8E00,%dx edx的低16位组装完毕。

中断处理过程就是ignore_int,用于在屏幕上打印一个’c’.

ignore_int:
    push %ds
    pushl %eax
    movl $0x10, %eax
    mov %ax, %ds       #上一行和此行用内核数据段加载ds
    movl $67, %eax     #打印字符'c',实际上用AL来传参
    call write_char    #调用过程 write_char
    popl %eax
    pop %ds
    iret

注意write_char这个过程没有指定DS,但是确引用了DS,比如指令movl scr_loc, %ebx. 所以在调用write_char之前,一定要给DS赋合适的值。

write_char这个过程,我已经在代码后面添加了注释。

write_char:
    push %gs
    pushl %ebx
    mov $SCRN_SEL, %ebx #SCRN_SEL是显存段的选择子
    mov %bx, %gs        #gs指向显存段
    movl scr_loc, %ebx  #scr_loc处存放的是显示位置
    shl $1, %ebx        #ebx*2,得到偏移,因为一个字符用2个字节来描述
    movb %al, %gs:(%ebx) #al中是字符的ASCII码,属性用默认的
    shr $1, %ebx       #还原ebx
    incl %ebx          #ebx自增1,算出下一个位置
    cmpl $2000, %ebx   #比较ebx和2000
    jb 1f              #若 ebx < 2000 则跳转到1
    movl $0, %ebx      #说明ebx==2000,因为位置只有0~1999,所以把ebx置为0
1:  movl %ebx, scr_loc #把ebx存入scr_loc处,更新显示位置
    popl %ebx
    pop %gs
    ret

可以看出,write_char的功能是把AL中的字符打印到屏幕上。
位置由scr_loc处存储的4字节的值指定(实际上取值0~1999),打印后更新位置(计数加1)。

scr_loc:.long 0 #代码中留出了4字节存放位置

填写IDT

lea idt,%edi表示把idt处的有效地址加载到edi.

idt标号处的代码是:

.align 8
idt:    .fill 256,8,0   

fill伪指令的格式是
.fill repeat,size,value
表示产生repeat个大小为size字节的重复拷贝。size最大是8,size字节的值是value.
所以,.fill 256,8,0表示产生8*256字节,全部用0填充。IDT最多可有256个描述符,每个描述符占8个字节。

mov $256,%ecx
rp_sidt:
    movl %eax,(%edi)
    movl %edx,4(%edi)
    addl $8,%edi
    dec %ecx     #当ecx为0时,会使ZF=0
    jne rp_sidt  #若ZF!=0则跳转到rp_sidt

Intel语法的间接内存引用的格式为:
section:[base + index * scale + displacement]
而在AT&T语法中对应的形式为:
section:displacement (base, index, scale)
其中,base和index是任意的32-bit base和index寄存器。scale可以取值1,2,4,8。如果不指定scale值,则默认值为1。section可以指定任意的段寄存器作为段前缀,默认的段寄存器在不同的情况下不一样。

举例:

IntelAT&T
[base + index * scale + displacement]section:displacement(base, index, scale)
[eax + _variable]_variable(%eax)
[eax * 4 + _array]_array(, %eax, 4)
[ebx + eax * 8 + _array]_array(%ebx, %eax, 8)

所以,movl %eax,(%edi)表示把eax的值传送到地址edi处,即用eax填充IDT表的0~3字节;movl %edx,4(%edi)表示把edx的值传送到地址[edi+4]处,即用edx填充IDT表的4~7字节;这样,IDT表中第0个中断门就安装好了。同理,循环安装,一共是256个中断门。

加载IDTR

lidt lidt_opcode 加载IDTR寄存器,lidt_opcode处定义了6个字节。前2字节是界限值,界限值是表的总长度减去1;后4字节是IDT的线性基地址。因为本文件运行时的起始地址就在物理地址0处,所以线性基地址就是idt表示的值。

lidt_opcode:
    .word 256*8-1   # idt contains 256 entries
    .long idt       # This will be rewrite by code. 

3. setup_gdt

这个过程就一句话

setup_gdt:
    lgdt lgdt_opcode
    ret

加载GDTR寄存器。
看一下 lgdt_opcode 处都有什么:

lgdt_opcode:
    .word (end_gdt-gdt)-1   
    .long gdt       # This will be rewrite by code.

前2字节是GDT的界限值,后4字节是GDT的线性基地址。

gdt:
    .quad 0x0000000000000000    /* NULL descriptor */
    .quad 0x00c09a00000007ff    /* 8Mb 0x08, base = 0x00000 */
    .quad 0x00c09200000007ff    /* 8Mb 0x10 */
    .quad 0x00c0920b80000002    /* screen 0x18 - for display */

    .word 0x0068, tss0, 0xe900, 0x0 # TSS0 descr 0x20
    .word 0x0040, ldt0, 0xe200, 0x0 # LDT0 descr 0x28
    .word 0x0068, tss1, 0xe900, 0x0 # TSS1 descr 0x30
    .word 0x0040, ldt1, 0xe200, 0x0 # LDT1 descr 0x38
end_gdt:

一共定义了8个段描述符。

索引号选择子描述符类型基地址段界限粒度PDPL备注
0-空描述符------
10x08代码段00X7FF4KB10内核代码段,非一致性,可读
20x10数据段00X7FF4KB10内核数据段,向上扩展,可写
30x18数据段0XB80000X24KB10内核显存段,向上扩展,可写
40x20TSS段tss00X681B13任务0的TSS段,不忙
50x28LDT段ldt00X401B13任务0的LDT描述符
60x30TSS段tss10X681B13任务1的TSS段,不忙
70x38LDT段ldt10X401B13任务1的LDT描述符

4. 重新加载段寄存器

因为GDT的内容改变了,所以应该重新加载所有段寄存器。

    movl $0x10,%eax      # reload all the segment registers
    mov %ax,%ds       # after changing gdt. 
    mov %ax,%es
    mov %ax,%fs
    mov %ax,%gs
    lss init_stack,%esp

注意:因为内核代码段和boot.s文件中的定义一样,所以不用重新加载CS

5. 设置定时芯片8253

关于这个定时芯片可以参考我的博文http://blog.csdn.net/longintchar/article/details/78885556

Intel 8253芯片是可编程计数器/定时器。该芯片提供了3个独立的16位计数器通道,每个通道可以工作在不同的工作方式下。通过向8253写入一个控制字和一个初始计数值,就可以使它开始计数。

控制字格式如下图:
这里写图片描述

代码中写入的控制字是0x36,选中通道0,先读写低字节再读写高字节,工作方式3,采用二进制计数。
通道0的端口是0x40, 先向其写入初始计数值的低字节,再写入初始计数值的高字节。

假设N为初始计数值。在工作方式3下,方波的频率是输入时钟频率的N分之一,又因为计数器的输入时钟频率是1.193180MHz=1193180Hz,所以

     1193180/N = 方波的频率(Hz)

movl $11930, %eax表示计数值N=11930,1193180/11930约等于100,
所以方波的频率是100Hz,周期是10ms,也就是10ms产生一个方波上升沿,此上升沿可以产生中断请求,即10ms产生一次中断。

# setup up timer 8253 chip.
    movb $0x36, %al   # al中是控制字
    movl $0x43, %edx  # 端口是0x43
    outb %al, %dx     # 把al中的控制字写入端口0x43
    movl $11930, %eax # timer frequency 100 HZ 
    movl $0x40, %edx  # 端口是0x40
    outb %al, %dx     # 先写低字节
    movb %ah, %al     # 再写高字节
    outb %al, %dx

【未完待续】

  • 6
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
LinuxCNC是一种开源的工业控制软件,被用于控制数控机床的运动。它的代码解析涉及到软件架构、功能模块以及相关算法等方面。 首先,LinuxCNC的代码遵循模块化的结构,通过各个独立且可重用的模块来实现不同的功能。一些常见的模块包括运动控制模块、插补算法模块、轴驱动模块等。 在功能方面,LinuxCNC提供了丰富的功能,包括插补控制、轴运动控制、输入/输出控制、通信接口等。其中,插补控制模块实现了高精度的插补算法,可以实现复杂的路径规划和轨迹控制。轴运动控制模块负责接收指令并控制电机驱动器进行运动。输入/输出控制模块用于与外部设备进行数据交互,如传感器和执行器。通信接口模块负责与上位机或其他设备进行通信,实现数据传输和指令控制等。 此外,LinuxCNC还有一些重要的算法。例如,PID控制算法用于实现轴驱动模块中的位置控制和速度控制。还有反馈控制算法用于实现闭环控制,通过传感器的反馈信号对系统进行实时调整。 总之,LinuxCNC的代码解析涉及到多个方面,包括代码结构、功能模块和算法等。这些模块和算法的实现使得LinuxCNC成为一款功能强大且可扩展的工业控制软件。通过深入理解代码,可以更好地理解其工作原理和实现机制,从而为用户提供更可靠、高效的数控机床控制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值