1 影音元数据

1)首先介绍一个影音元数据库的网站TMDB。

https://www.themoviedb.org/

注册账号并获取api的key

2)字幕网站

https://www.opensubtitles.com/

注册账号并获取api的key

3)关于刮削的文件命名

基本信息:

原剧集语言
固定演员

参考:

https://www.bilibili.com/read/cv3275648/

https://www.luxiyue.com/personal/%E4%B8%80%E6%96%87%E6%90%9E%E6%87%82%E7%94%B5%E5%BD%B1%E6%96%87%E4%BB%B6%E5%91%BD%E5%90%8D%E8%A7%84%E5%88%99/

https://blog.iplayloli.com/sharing-of-tinymeediamanger-renaming-rules.html

8.4 影视刮削工具

下载刮削工具 TinyMediaManager

下载地址:https://www.tinymediamanager.org/

批量重命名工具 Advance Renamer

下载地址:https://www.advancedrenamer.com/

assets/nas017.png

assets/nas019.png

8.2 影音命名规则

影音分为两种。
1)MOVIE:电影
2)TVSHOW:剧集(例如连续剧或者番剧)

8.2.1电影命名规则

文件夹命名为
影片名称(年份)

电影命名:
电影中文名.电影原始名.发行年份.分辨率.影片来源.影片编码.影片格式
加勒比海盗2:聚魂棺.Pirates.of.the.Caribbean.Dead.Mans.Chest.2006.BluRay.720p.x264.AC3-WOFEI.mkv

8.2.1剧集命名规则

Friends.S03E10.1080p.x264.mkv
节目名称.第几季第几集.分辨率.影片编码.视频格式

8.3 字幕编辑

subtitleEdit 字幕编辑

.default.字母格式: 在文件名末尾附加字幕来将字幕标记为默认字幕

8.4 刮削后整理:重命名

8.4.1 电影
文件夹重命名配置:

${title}${ - ,edition,} (${,year,})

文件命名配置:
${title}${if !movie.title=movie.originalTitle}${.originalTitle}${end}${.,year,}${.,edition,}${.,mediaSource,}${.,videoFormat,}${.,videoCodec,}.${videoBitDepth}bit.${.,audioCodec;upper,}

assets/nas020.png

8.4.2电视

1)文件夹重命名配置:
${showTitle} (${showYear})

2)特别篇或者OVA目录
文件命名配置:
Specials

3)季命名
Season ${seasonNr}

3)文件命名
${tvShow.title}${if !tvShow.title=tvShow.originalTitle}${.,tvShow.originalTitle,}${end}.S${seasonNr2}E${episodeNr2}${.,videoFormat,;replace(format.csv)}${.,videoCodec,;replace(codec.csv)}

第几季第几集用S季数E集数表示
例如,第八季第五集: S08E05

  1. 特别篇或者OVA目录文件
    季命名以S00开始

好的文件命名是决定刮削是否准确的关键

效果如图

assets/nas018.png

1.9 番号整理

1.9.1 软件下载安装

使用MDXC进行番号刮削

项目地址:
https://github.com/sqzw-x/mdcx

下载地址:
https://github.com/sqzw-x/mdcx/releases/tag/120240321

配置如下:

assets/nas013.png

assets/nas014.png

assets/nas015.png

assets/nas016.png

具体不能多说了,自己研究吧。展示下刮削效果

assets/nas021.png

assets/nas022.png

1.9.2 番号命名规则

刮削前文件名称整理

  1. 目录名称不动
  2. 文件名称修改为番号名称

刮削重命名配置

  1. 目录名称不动
  2. 文件名称修改为番号名称

音乐刮削

下载Music Tag音乐标签客户端
(或者Mp3Tag,需要自己配置源)

文件-添加目录:添加音乐目录

工具栏-点击自动匹配标签:自动匹配项,除了标题和艺术家都选上

文件-重命名:按照 (艺术家 - 标题) 命名

1.9 番号整理

1.9.1 软件下载安装

使用MDXC进行番号刮削

项目地址:
https://github.com/sqzw-x/mdcx

下载地址:
https://github.com/sqzw-x/mdcx/releases/tag/120240321

配置如下:

assets/nas013.png

assets/nas014.png

assets/nas015.png

assets/nas016.png

具体不能多说了,自己研究吧。展示下刮削效果

assets/nas021.png

assets/nas022.png

1.9.2 番号命名规则

刮削前文件名称整理

  1. 目录名称不动
  2. 文件名称修改为番号名称

刮削重命名配置

  1. 目录名称不动
  2. 文件名称修改为番号名称

1 方案说明

硬件搭配方便,主要做出了一下三种硬件方案。

  1. 省电方案
硬件 型号 价格
CPU 8100 180
主板 华南B250 250
内存 玖合DDR4-8GB X2 200
m2固态 铠侠SD10-1T 480
电源 ds-atx 100
电源 150dc电源 100
机箱 盘隆-4盘位机箱 260

8100带T和不带T,待机情况下功耗没有区别。
待机功耗。以上13W。

2 硬盘柜版

说明。采用小主机+usb+外接硬盘柜方案。

硬件 型号 价格
CPU 七喜N100准系统小主机 700
内存 玖合dd4笔记本内存3200-16G 180
m2固态 铠侠SD10-1T 480
硬盘柜 奥睿科5盘位硬盘柜-无RAID 800
总计 2160元

待机功耗。以上20W。

光标寄存器

光标位置信息位于显卡的2个光标寄存器中,总共16位,分为高位和低位存储。

例如。

标准VGA文本模式为 25 行 80 列

例如:
pos = 0 :表示 位于 1 行 0 列
pos = 27 :表示 位于 2 行 2 列
pos =1999 : 最右下角

读取光标值

光标寄存器的端口号是 0x3d4 和 0x3d5

0x3d4用来写入变量
0x3d5用来获取光标的值

端口 读写
0x3d4 0xe0 写入
0x3d5 光标高8位值 读取
0x3d4 0xf0 写入
0x3d5 光标低8位值 读取

获取光标和设置光标

1
2
3
4
uint16 get_cursor();

void set_cursor(uint16 pos);

实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
uint16 get_cursor()
{
//get high cursor value
io_out8(0x3d4, 0x0e);
uint8 cursor_high = io_in8(0x3d5);
//get low cursor value
io_out8(0x3d4, 0x0f);
uint8 cursor_low = io_in8(0x3d5);
//high + low
return (cursor_high << 8) + (cursor_low & 0xff);
}

void set_cursor(uint16 pos)
{
uint8 cursor_high = pos >> 8;
uint8 cursor_low = pos & 0xff;
//set high cursor value
io_out8(0x3d4, 0x0e);
io_out8(0x3d5, cursor_high);
//set low cursor value
io_out8(0x3d4, 0x0f);
io_out8(0x3d5, cursor_low);
}

说明:
pos代表设置位置,标准VGA文本模式为 25 行 80 列,pos取值从0到1999. 通过get_cursor函数获取光标现在的位置,通过set_cursor设置光标在25 行 80 列文本模式的位置。

打印调试

为了方便测试数据,需要在控制台进行数据输出。linux内核的控制台输出使用printk.c文件,调用vprintf方法。

vprintf方法可以使用参数列表发送格式化输出到标准输出 stdout。

vprintf方法可以使用C语言格式化输出。

实现stdout输出

为了简单方便,就不使用c标准库的vprintf方法了。重新定义了简单的控制台输出方法。

创建printk文件,订阅打印方法。

字符打印时,需要对回车,换行,tab,空格等字符进行特殊处理

字符打印流程

images/3_5_1.png

代码

printk.h

1
2
3
4
5
6
7
8
9
10
11
void print_char(char ch);

void print_string(char* str);

void print_int16(int16 num);

void print_int32(int32 num);

void print_int64(int64 num);

void print_pointer(uint32 addr);

printk.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172


#include "../include/stdint.h"
#include "../include/io.h"
#include "../include/printk.h"

#define VGA_BASE 0xB8000
#define ROW 25
#define COL 80

static char printBuf[1024];

uint16 get_cursor()
{
//get high cursor value
io_out8(0x3d4, 0x0e);
uint8 cursor_high = io_in8(0x3d5);
//get low cursor value
io_out8(0x3d4, 0x0f);
uint8 cursor_low = io_in8(0x3d5);
//high + low
return (cursor_high << 8) + (cursor_low & 0xff);
}

void set_cursor(uint16 pos)
{
uint8 cursor_high = pos >> 8;
uint8 cursor_low = pos & 0xff;
//set high cursor value
io_out8(0x3d4, 0x0e);
io_out8(0x3d5, cursor_high);
//set low cursor value
io_out8(0x3d4, 0x0f);
io_out8(0x3d5, cursor_low);
}


void print_char(char ch)
{

uint16 pos = get_cursor();
char *pvga = (char *)VGA_BASE;

//字符
switch (ch)
{
case 0x0d: //RETURN
pos = (pos / COL) * COL;
break;
case 0x0a: //NEW LINE
pos = pos + COL;
pos = (pos / COL) * COL;
break;
case 0x09: //TAB
pos=pos+4;
break;
case 0x08: //BACKSPACE
pos--;
*(pvga + pos * 2) = 0x00;
break;
default:
*(pvga + pos * 2) = ch;
*(pvga + pos * 2 + 1) = 0x17; //蓝底白字
pos++;
}
//字符超出时, 滚屏
if (pos + 1 > ROW * COL )
{
int display = (ROW -1) * COL;
pos = pos - COL;
for (int i = 0; i < display; i++)
{
*(pvga + 0x00 + i * 2) = *(pvga + 0xa0 + i * 2);
}
for (int i = 0; i < COL ; i++)
{
*(pvga + display * 2 + i * 2) = 0x0;
}
}
set_cursor(pos);
}

void print_string(char *chs)
{
int i = 0;
while (*(chs + i) != 0)
{
print_char(*(chs + i));
i++;
}
}


void print_int16(int16 num)
{
int i = 0;
char *chs = (char *)printBuf; //need point mem alloc
do
{
*(chs + i) = (char)(num % 10 + '0'); //取下一个数字
i++;

} while ((num /= 10) > 0); //删除该数字
for (int j = i-1; j >= 0; j--)
{ //生成的数字是逆序的,所以要逆序输出
print_char(*(chs + j));
*(chs + j) = 0;
}
}

void print_int32(int32 num)
{
int i = 0;
char *chs = (char *)printBuf; //need point mem alloc
do
{
*(chs + i) = (char)(num % 10 + '0'); //取下一个数字
i++;

} while ((num /= 10) > 0); //删除该数字
for (int j = i-1 ; j >= 0; j--)
{ //生成的数字是逆序的,所以要逆序输出
print_char(*(chs + j));
*(chs + j) = 0;
}
}

void print_int64(int64 num)
{
//32位系统计算64位无法直接运行,需要自己实现方法,先留空
}

void print_pointer(uint32 addr)
{
print_char('0');
print_char('x');
int i = 0;
char *chs = (char *)printBuf; //need point mem alloc
do
{
int ch = addr % 16;
if(ch==0x0f) {
*(chs + i)='F';
} else if(ch==0x0e) {
*(chs + i)='E';
} else if(ch==0x0d) {
*(chs + i)='D';
} else if(ch==0x0c) {
*(chs + i)='C';
} else if(ch==0x0b) {
*(chs + i)='B';
} else if(ch==0x0a) {
*(chs + i)='A';
} else {
*(chs + i) = (char)(addr % 16 + '0'); //取下一个数字
}
i++;

} while ((addr /= 16) > 0); //删除该数字
for (int j = 7; j > i-1; j--)
{ //补充0
print_char('0');
*(chs + j) = 0;
}
for (int j = i-1; j >= 0; j--)
{ //生成的数字是逆序的,所以要逆序输出
print_char(*(chs + j));
*(chs + j) = 0;
}
}


修改加载文件,输出打印调试信息

1
2
3
4
5
6
7
#include "../include/printk.h"

int _start(){
print_string("rast os system begin start...\0");
fin:
goto fin;
}

显示结果如下:

images/3_5_2.png

Makefile文件更新
Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# tools
PLATFORM=Linux
NASM=nasm
BOCHS=bochs
BXIMAGE=bximage

# args
boot=boot
asm=asm
lib=lib
build=build
ENTRY_POINT=0x10000
CFLAGS=-m32 -c -nostdinc -nostdlib -fno-builtin -Wall -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS=-m elf_i386 -s -e _start -Ttext $(ENTRY_POINT)

target: $(build)/rastos.img
@echo "build img completed"

$(build)/rastos.img:$(build)/boot.bin $(build)/loader.bin $(build)/loaderELF.bin
$(BXIMAGE) -func=create -imgmode=flat -hd=16M -q $(build)/rastos.img
sleep 2
dd if=$(build)/boot.bin of=$(build)/rastos.img bs=512 count=1 conv=notrunc
dd if=$(build)/loader.bin of=$(build)/rastos.img bs=512 count=1 seek=1 conv=notrunc
dd if=$(build)/loaderELF.bin of=$(build)/rastos.img bs=512 count=30 seek=2 conv=notrunc


$(build)/loaderELF.bin: $(build)/loaderELF.o $(build)/io.o $(build)/printk.o
$(LD) -o $@ $^ $(LDFLAGS)

$(build)/loaderELF.o: $(boot)/loaderELF.c
$(CC) $(boot)/loaderELF.c -o $(build)/loaderELF.o $(CFLAGS)

$(build)/io.o:
$(NASM) -f elf -o $(build)/io.o $(asm)/io.asm

$(build)/%.bin: $(boot)/%.asm
$(NASM) -f bin -o $(build)/$*.bin $(boot)/$*.asm

$(build)/%.o:
$(CC) -o $(build)/$*.o $(lib)/$*.c -I include $(CFLAGS)

prepare: $(build)
@echo "prepare dir $(build)"
ifeq ($(build), $(wildcard $(build)))
@echo "build directory exist..."
else
mkdir -p $(build)
endif

clean:
@echo "clean dir $(build)"
rm -rf $(build)/*

platform:
@echo $(PLATFORM)

检测内存大小

因为需要使用BIOS中断,所以应该在实模式下获取内存信息,并存在固定的内存位置,然后在保护模式下解析出内存信息:

INT 15H:System Service 系统服务中断

功能如下三种:

  • AH = 0x88 :检测内存大小,最多检测出64MB。

入口参数:
AH = 0x88

出口参数:
CF=表示为标志位,0调用成功,1调用出错
AX=扩展内存字节数(以1KB为单位),不包含低端的1MB内存

  • AX= 0xE801 : 检测内存大小,最多检测出4GB内存。

入口参数:

AX= 0xE801
出口参数:
CF=表示为标志位,0调用成功,1调用出错
AX=扩展内存字节数(以1KB为单位),存储15M以下的容量大小.
BX=扩展内存字节数(以64KB为单位),存储16MB-4GB的容量大小.

  • EAX=0xE820: 检测内存大小, 全部内存大小

入口参数:
EAX= 0xE820
EBX=0, ARDS 的后继值,第一次调用设置为0
ECX=20 , ARDS字节大小.
EDX=0x534D4150, 签名标记的默认值.代表字符串SMAP的ASCII码.
ES: DI= ARDS缓冲区地址,内存信息会写入此区域

出口参数:
CF=表示为标志位,0调用成功,1调用出错
EAX=0x534D4150, 签名标记的默认值.
EBX= X, ARDS 的后继值, BIOS自动修改调整, 次寄存器无需手动修改. 如果返回为0,表示已经是最后一个 ARDS 的结构.
ES: DI= ARDS缓冲区地址, 和输入值一样. 内存信息已经被写入此区域.

ARDS结构 (20个字节)

调用内存中断后,BIOS将内存信息存在指定地址,其数据格式为ARDS结构。如下:

Offset Name Description
0-4 BaseAddrLow 基地址的低32位
4-8 BaseAddrHigh 基地址的高32位
8-12 LengthLow 长度(字节)的低32位
12-16 LengthHigh 长度(字节)的高32位
16-20 Type 这个地址范围的地址类型

其中Type的取值及其意义如下:

Type取值
1: AddressRangeMemory,这个内存段是一段可以被OS使用的RAM
2:AddressRangeReserved,这个地址段正在被使用,或者被系统保留,所以一定不要被OS使用
Other: Undefined,保留为未来使用,任何其它值都必需被OS认为是AddressRangeReserved

定义结构如下:

1
2
3
4
5
6
7
8
struct e820map {
int map_num;
struct {
long long addr;
long long size;
long type;
} map[E820MAX];
};
1
2
3
4
5
6
7
8
9
10
11
## 中断获取内存信息

因为需要使用int中断获取内存,所以必须回到实模式下。在loader.bin文件中读取内存信息并存储到一个固定位置。指定0x1000位置


代码:

boot.inc

```assembly
INFO_MEM_ADDR  equ 0x1000

loader.asm

1
2
3
4
5
6
7
;----------------------
;获取内存信息到0x0000
;----------------------
mov ax, INFO_MEM_ADDR/16
mov es, ax
mov di, 4
call ReadMemInfo

ReadMemInfo方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
[bits 16]
;----------------------------
;读取内存信息, 读取到 0x0000
;es:di 读取后存储到的内存地址
;----------------------------
ReadMemInfo:
mov dword [INFO_MEM_ADDR], 0
mov edx, 0x0534d4150
mov ebx, 0

.mem_get_loop:
mov eax, 0x0e820 ; 检测内存大小
mov ecx, 128
int 0x15
jc .mem_get_fail
add di,20
inc dword [INFO_MEM_ADDR] ;记录内存ADS数量
cmp ebx, 0 ;ebx为0代表全部查找完成
jnz .mem_get_loop
jmp .mem_get_ok
.mem_get_fail:
mov si,ReadMemFail ; 地址放入si
mov dh,0 ; 设置显示行
mov dl,0 ; 设置显示列
call PrintString ; 调用函数
jmp $
.mem_get_ok:
;mov si,ReadMemOk ; 地址放入si
;mov dh,0 ; 设置显示行
;mov dl,0 ; 设置显示列
;call PrintString ; 调用函数
ret


; ------------------------------------------------------------------------
; 显示字符串函数:PrintString
; 参数:
; si = 字符串开始地址,
; dh = 第N行,0开始
; dl = 第N列,0开始
; ------------------------------------------------------------------------
PrintString:
mov cx,0 ;BIOS中断参数:显示字符串长度
mov bx,si
.s1:;获取字符串长度
mov al,[bx] ;读取1个字节到al
add bx,1 ;读取下个字节
cmp al,0 ;是否以0结束
je .s2
add cx,1 ;计数器
jmp .s1
.s2:;显示字符串
mov bx,si
mov bp,bx
mov ax,ds
mov es,ax ;BIOS中断参数:计算[ES:BP]为显示字符串开始地址

mov ah,0x13 ;BIOS中断参数:显示文字串
mov al,0x01 ;BIOS中断参数:文本输出方式(40×25 16色 文本)
mov bh,0x0 ;BIOS中断参数:指定分页为0
mov bl,0x1F ;BIOS中断参数:指定白色文字
mov dl,0 ;列号为0
int 0x10 ;调用BIOS中断操作显卡。输出字符串
ret

; ------------------------------------------------------------------------
; 字符串常量
; ------------------------------------------------------------------------
ReadMemFail: db "read mem fail!",0x0D,0x0A,0
ReadMemOk:  db "read mem ok!",0x0D,0x0A,0

在C语言中读取内存信息

我们在loadELF.bin文件尝试读取获取的内存大小

mem.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef __LIB_MEM_H
#define __LIB_MEM_H

#define MEM_INFO_ADDR 0x1000
#define E820MAX 128

#include "stdint.h"

struct e820map {
int map_num;
struct {
uint32 addr;
uint32 addr_unused;
uint32 size;
uint32 size_unused;
uint32 type;
} map[E820MAX];
};

void init_mem(void);

#endif

mem.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "../include/mem.h"
#include "../include/printk.h"

void init_mem(){

//获取内存信息
struct e820map *mem_info = (struct e820map *) MEM_INFO_ADDR;
print_string("MemInfo:\n");
print_string("MapNum:");
print_int16(mem_info->map_num);
print_char('\n');

//打印内存信息
for(int i = 0; i<mem_info->map_num;i++) {
print_string("Map:");
print_pointer(mem_info->map[i].addr);
print_string(":");
print_pointer(mem_info->map[i].size);
print_string(":");
print_int32(mem_info->map[i].type);
print_string("\n");
}

}

执行打印

1
2
3
4
5
6
7
8
9
10
#include "../include/printk.h"
#include "../include/mem.h"

int _start(){
print_string("rast os system begin start...\n");
print_string("read mem info start...\n");
init_mem();
fin:
goto fin;
}

结果如下:

./images/3_6_1.png

转换内容:

地址1:start= 0x00000 ,len= 0x9F000,type=1

地址2:start= 0x9F000 ,len= 0x1000,type=2(系统保留)

地址3:start= 0xE8000 ,len= 0x18000,type=2(系统保留)

地址4:start=0x100000, len=0x1EF0000,type=1

地址5:start=0x1FF0000, len=0x100000,type=3

地址6:start=0xFFFC0000, len=0x40000,type=2(系统保留)

为什么要分页

**1. 实模式分段: **

  • 原因: cpu段寄存器太小, 和cpu总线不匹配. ( cpu段寄存器16位,cpu总线20位.)

  • 解决方案:通过 16位段寄存器 , 16位指针寄存器 ,使用2个16位寄存器配合达到20位总线寻址.

  • 段寄存器:此时段寄存器是物理地址的偏移地址。

  • 段地址:此时段地址是实际的物理地址

**2. 保护模式分段: **

  • 原因: 实模式下20位总线,寻址只有1M,需要突破实模式1M内存寻址的限制。保护模式下使用32位总线,寻址可以达到4G,使用到更多内存。

  • 解决方法: 创建全局描述符表, 先根据全局描述符表定位到分段信息,然后根据分段信息定义的起始物理地址 和 当前偏移地址找到实际物理地址。

  • 段寄存器:此时段寄存器是全局描述符表的分段信息的选择子序号。

  • 段地址:此时段地址是实际的物理地址

3. 分页模式:

  • 原因: 实际物理地址存在范围比较小, 且不连续的问题. 碎片化后可使用内存减少。

  • 解决方法: 创建页表,建立虚拟地址和物理地址的映射,加载的是是虚拟地址,然后根据虚拟地址查到物理地址并执行代码

  • 段寄存器:此时段寄存器是全局描述符表的分段信息的选择子序号。

  • 段地址:此时段地址是映射的虚拟地址,需要查找页表转换成实际分配的物理地址。

不同架构的分页机制

32位下分页
32位系统,内存支持到4G,使用二级页表,就可以满足要求了、

64位下分页
64位系统,支持到128G内存,使用二级页表无法满足要求,此时需要使用三级,四级页表了。

Linux采用的四级页表目录的大小有所不同:对于i386而言,仅采用二级页表,即页上层目录和页中层目录长度为0;对于启用PAE的i386,采用了三级页表,即页上层目录长度为0;对于64位体系结构,可以采用三级或四级页表,具体选择由硬件决定。

从上述过程中,可以看出,对虚拟地址的分级解析过程,实际上就是不断深入页表层次,逐渐定位到最终地址的过程,所以这一过程被叫做page talbe walk

至于这种做法为什么能节省内存,举个更简单的例子更容易明白。比如要记录16个球场的使用情况,每张纸能记录4个场地的情况。采用4+4+4+4,共4张纸即可记录,但问题是球场使用得很少,有时候一整张纸记录的4个球场都没人使用。于是,采用4 x 4方案,即把16个球场分为4组,同样每张纸刚好能记录4组情况。这样,使用一张纸A来记录4个分组球场情况,当某个球场在使用时,只要额外使用多一张纸B来记录该球场,同时,在A上记录”某球场由纸B在记录”即可。这样在大部分球场使用很少的情况下,只要很少的纸即困记录,当有球场被使用,有需要再用额外的纸来记录,当不用就擦除。这里一个很重要的前提就是:局部性

页目录和页表

1. 一级页表

物理内存分块:
在分页的情况下,将物理内存划分为最小4K (即2^12寻址范围)的内存块。

页表:
4G内存下,使用4G/4K=1048576=1M个页。一个页地址占4个字节,需要1048576 x 页地址4字节 = 4M空间。

页地址:
一个页地址占4个字节,高20位为页号,低12位为物理内存分块的偏移地址

虚拟地址映射:
页地址:

  • 高20位:页号(1M)。
  • 低12位:物理内存分块的偏移地址(4K)。
    物理地址:
  • 虚拟地址 = 页号(高20位) + 物理内存分块的偏移地址(低12位)
  • 物理地址 = [ (页起始地址 + 页号 x 页表项大小) -> 物理内存开始地址 ] + 物理内存分块的偏移地址
  • 页表项大小 :4字节

一级页表存储结构:

一级页表
0页:高20位页号 0页:低12位偏移地址
1页:高20位页号 1页:低12位偏移地址
1048576页:高20位页号 1048576页:低12位偏移地址

2. 二级页表

一级页表大小是4M, 占用内存过大。如果把地址拆分开存放,用两个页表来存放地址,每个页表只需要1024K。(第一个表存页开始地址,第二个存物理内存开始地址)。

此时第一个称为页目录,第二个称为页表,页地址大小为4字节。

继续把一级的1M大页表 。拆分开1024 x 1024的页,每个页大小1K x 页地址4字节。

一级页表虚拟地址:1M页地址(20位) + 4K物理分块偏移地址(12位)
二级页表虚拟地址:1K页目录地址(10位) + 1K页地址(10位) + 4K物理分块偏移地址(12位)
页表项:

虚拟地址映射
页地址:

  • 高10位:页目录号(1K)

  • 中10位:页号(1K)

  • 低12位:物理内存分块的偏移地址(4K)。
    物理地址:

  • 虚拟地址 = 页目录序号(高10位) + 页序号(中10位) + 物理内存分块的偏移地址(低12位)

  • 物理地址 = [([页目录表起始地址 + 页目录号 x 页表大小) -> 页表起始地址] + 页号 x 页表项大小 ) -> 物理内存开始地址 ] + 物理内存分块的偏移地址

  • 页表大小:4K字节,页表项大小 :4字节。

页目录表
0目录:高20位页表起始地址
1目录:高20位页表起始地址
1024页:高20位页表起始地址
一级页表(按需分配)
0页:物理页起始地址
1页:物理页起始地址
1024页:物理页起始地址
  • 为什么要使用多级页表

    多级页表优势:

    1.可以离散存储页表。

    2.在某种意义上节省页表内存空间。

    多级页表劣势:

    增加寻址次数,从而延长访存时间。

    那么使用多级页表比使用以及页表有没有什么劣势呢?

    当然是有的。比如:使用以及页表时,读取内存中一页内容需要2次访问内存,第一次是访问页表项,第二次是访问要读取的一页数据。但如果是使用二级页表的话,就需要3次访问内存了,第一次访问页目录项,第二次访问页表项,第三次访问要读取的一页数据。访存次数的增加也就意味着访问数据所花费的总时间增加。

4. 动态分页

一个页表项,寻址范围为4M,16M需要4个页表项,4G需要1024个列表项。

进入分页模式

1. 进入分页模式

代码:

1
2
3
4
5
;------------------
;进入分页模式
mov eax, cr0
or eax, 0x80000000 ;设置第31位为1
mov cr0, eax

2.加载CR3

加载页目录基址到cr3寄存器

1
mov cr3, edx

头文件io.h

汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
io_load_page:   ;void io_load_page(uint32 pde_address) 
;cr3
mov edx, [esp+4]
mov cr3, edx
ret

io_switch_page: ;void io_switch_page(void)
;cr0
mov eax, cr0
or eax, 0x80000000 ;设置第31位为1
mov cr0, eax
ret

页表到页表的寻址

页目录基址

定义页目录基址为0x100000位置。将此区域设置页目录的保存地址。在0x100000此处写入分页信息。

创建1个临时分页,此临时分页大小为4G,并且线性地址和物理地址从0开始一一对应。

代码:

io.h

1
2
3
void io_load_page(uint32 pte_addr);

void io_to_page();

io.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

global io_load_page, io_to_page

io_load_page: ;void io_load_page(uint32 pde_address)
;cr3
mov edx, [esp+4]
mov cr3, edx
ret

io_switch_page: ;void io_switch_page(void)
;cr0
mov eax, cr0
or eax, 0x80000000 ;设置第31位为1
mov cr0, eax
ret

page.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef __LIB_PAGE_H
#define __LIB_PAGE_H

#define PDE_START 0x100000
#define PTE_START 0x101000

#define PG_P 1
#define PG_RW_R 0
#define PG_RW_W 1 << 1
#define PG_US_S 0
#define PG_US_U 1 << 2
#define PG_PDE_ATTR PG_US_U | PG_RW_W | PG_P
#define PG_PTE_ATTR PG_US_U | PG_RW_W | PG_P


void init_page();

#endif

page.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

#include "../include/stdint.h"
#include "../include/io.h"
#include "../include/mem.h"
#include "../include/printk.h"

void init_page(){

//页目录
uint32 *page_dir = (uint32 *)PDE_START;
for(int i=0;i<1024;i++){
*(page_dir+i) = {0};
}

//页表
uint32 *page_table_dir = (uint32 *)PTE_START;
for(int i=0;i<1024;i++){
*(page_table_dir+i) = {0};
}

//初始化临时分页
for(int i=0;i<1024;i++){
*(page_table_dir+i) = { (i * 0x1000) | PG_PTE_ATTR };
}
*(page_dir) = { PTE_START | PG_PTE_ATTR };

//调用汇编,设置cr3和cr0,进入分页模式
print_string("io_load_page start\n");
io_load_page(PDE_START);
print_string("io_load_page ok\n");
io_switch_page();
print_string("to page mode ok\n");
}

运行结果
images/3_8_3.png

分页调试

1)查看是否分页模式切换成功
bochs执行creg。查看PG位是否为1,为1则开启成功
images/3_8_1.png

执行info tab,查看创建好的临时分页
images/3_8_2.png

外设端口

设备通常会提供一组寄存器来控制设备、读写设备和获取设备状态,即控制寄存器、数据寄存器和状态寄存器。

这些寄器可能位于I/O空间中,也可能位于内存空间中。当位于I/O空间时,通常被称为I/O端口;当位于内存空间时,对应的内存空间被称为I/O内存。

每个外设都是通过读写其寄存器来控制的。外设寄存器也称为I/O端口,通常包括:控制寄存器、状态寄存器和数据寄存器三大类。

## 外设I/O端口读写

X86的cpu可以直接读写以下三个地方的数据,读写三个地方的指令都是不同的,他们的空间也是分开的,这点要注意。

I/O端口是指设备控制器中可被CPU直接访问的寄存器,主要有以下三类寄存器。

  • 数据寄存器:用于缓存从设备送来的输入数据,或从CPU送来的输出数据。
  • 状态寄存器:保存设备的执行结果或状态信息,以供CPU读取。
  • 控制寄存器:由CPU写入,以便启动命令或更改设备模式。

I/O 端口要想能够被CPU访问,就要对各个端口进行编址,每个端口对应一个端口地址。而对 I/O端口的编址方式有与存储器独立编址和统一编址两种.

将外设的寄存器看成一个独立的地址空间,所以访问内存的指令不能用来访问这些寄存器,而要为对外设寄存器的读/写设置专用指令,如IN和OUT指令。这就是所谓的“ I/O端口”方式

端口读写指令

CPU对外设通过端口读写指令来进行读写数据。

端口读写指令

  • in cpu寄存器,端口地址 : 从端口中读取数据到CPU寄存器

  • out 端口地址 , cpu寄存器: 写入CPU寄存器的数据到端口中

  • 此处的CPU寄存器,为一个字节(8位),字(16位)或双字(32位),根据CPU寄存器的大小,从端口地址处读取不同的字节数。

include/stdint.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef __LIB_STDINT_H
#define __LIB_STDINT_H

typedef signed char int8;
typedef signed short int16;
typedef signed int int32;
typedef signed long long int64;

typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint32;
typedef unsigned long long uint64;

#endif

类型别名

c语言调用汇编

定义外设端口读写方法,C语言调用汇编

1)首先定义一个C语言头文件

2)使用汇编实现C语言头文件方法

3)C语言和汇编编译链接在一起

include/io.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#ifndef __LIB_IO_H
#define __LIB_IO_H

#include "stdint.h"

void io_cli();

void io_sti();

void io_hlt();

uint8 io_in8(uint16 port);

void io_out8(uint16 port, uint8 data);

uint16 io_in16(uint16 port);

void io_out16(uint16 port, uint16 data);

uint32 io_in32(uint16 port);

void io_out32(uint16 port, uint32 data);

#endif

asm/io.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
;io.asm
[bits 32]

global io_cli, io_sti, io_hlt
global io_in8, io_out8
global io_in16, io_out16
global io_in32, io_out32

[section .text]

io_cli: ;void io_cli
cli
ret

io_sti: ;void io_sti
sti
ret

io_hlt: ;void io_hlt
hlt
ret

io_in8: ; uint8 io_in8(uint16 port)
mov dx,[esp+4]
mov al,0
in al,dx
ret

io_out8: ; void io_out8(uint16 port,uint8 data)
mov dx,[esp+4]
mov al,[esp+8]
out dx, al
ret

io_in16: ; uint16 io_in16(uint16 port)
mov dx,[esp+4]
mov ax,0
in ax,dx
ret

io_out16: ; void io_out16(uint16 port,uint16 data)
mov dx,[esp+4]
mov ax,[esp+8]
out dx, ax
ret

io_in32: ; uint32 io_in32(uint16 port)
mov dx,[esp+4]
mov eax,0
in eax,dx
ret

io_out32: ; void io_out32(uint16 port,uint32 data)
mov dx,[esp+4]
mov eax,[esp+8]
out dx,eax
ret



ELF文件

上次说过了,其实目标文件和可执行文件都是ELF格式文件

ELF索引表

ELF 文件包括三个索引表

  • ELF Header: ELF文件头

    作用:

    1. 指定程序入口

    2. 定位Program  header  table位置

    3. 定位Section  header  table位置

  • Program  header  table:程序头表。

    作用:

    1)查询segment的位置(一个segment可能会包含多个Section)

    2)根据此表创建内存中创建映像

  • Section  header  table:节区头表。

    作用:

    1)存储文件节区的信息

    2)根据此表定位代码段,数据段位置

ELF内容区

  • 字符串表:interp,.strtable ,.shstrtab,.dynstr节区
  • 符号表:.symtab
  • 代码段:.text节区
  • 数据段:.data、.rodata 、.bss节区
  • 全局偏移表:.got节区
  • 过程链接表: .plt节区
  • 哈希表:指.hash节区
  • 编译器版信息:.comment

ELF字段类型

  • Elf32_Addr:4字节,无符号程序地址

  • Elf32_Half: 2字节,无符号中等整数

  • Elf32_Off:4 字节,无符号文件偏移

  • Elf32_SWord:4 字节,有符号大整数

  • Elf32_Word :4字节,无符号大整数

ELF的三种Header格式

ELF Header格式

字段 类型 长度 说明
ident char [16] 16 魔数
type Elf32_Half 2 文件类型
machine Elf32_Half 2 硬件平台
version Elf32_Word 4 版本
entry Elf32_Addr 4 程序进入点 <24>
phoff Elf32_Off 4 程序头表偏移量<28>
shoff Elf32_Off 4 节头表偏移量
flags Elf32_Word 4 处理器特定标志
ehsize Elf32_Half 2 ELF头部大小
phentsize Elf32_Half 2 程序头大小<42>
phnum Elf32_Half 2 程序头数量<44>
shentsize Elf32_Half 2 节头大小
shnum Elf32_Half 2 节头数量
shstrndx Elf32_Half 2 字符串表索引节头

ELF文件分析

查看文件头

readelf -h loader1.bin

images/5_3_1.png

Program Header 程序头格式

字段 类型 长度 说明
type Elf32_Word 4 段类型
offset Elf32_Off 4 偏移量 <4>
vaddr Elf32_Addr 4 内存虚拟地址 <8>
paddr Elf32_Addr 4 物理地址
filesz Elf32_Word 4 段大小(文件占用)<16>
memsz Elf32_Word 4 段大小(内存占用)
flag Elf32_Word 4 段标志
align Elf32_Word 4 段对齐

查看程序头:

$ readelf -l 文件名

images/4_1_1.png

Section Header节头

节头格式

字段 类型 长度 说明
name Elf32_Word 4 节名称
type Elf32_Word 4 节类型
flags Elf32_Word 4 节属性
addr Elf32_Addr 4 节区地址
offset Elf32_Off 2 偏移量
size Elf32_Word 4 节大小(文件)
link Elf32_Word 4 节区头部表索引链接
info Elf32_Word 4 附加信息
addralign Elf32_Word 4 地址对齐
entsize Elf32_Word 4 项目表长度

查看字段头:

$ readelf -S 文件名

images/4_1_2.png

符号表 Symbol table

其他

  • 查看全部信息

readelf -a loaderELF.o

  • 查看所有分段大小

size loaderELF.o

  • 查看分段内容

objdump -s loaderELF.o

  • 查看符号表

objdump -t loaderELF.o

  • 反编译

objdump -S loaderELF.o

内存复制

数据传送指令

movsb指令

movsb : 即字符串传送指令,这条指令按字节送数据。默认复制一个字节/

参数:

  • esi:数据源地址
  • edi:数据目标地址

rep movsb指令

rep movsb : 即字符串传送指令,复制多个字节

参数:

  • esi:数据源地址
  • edi:数据目标地址
  • ecx:复制字节数、

汇编代码

1
2
3
4
5
6
7
8
9
10
;------------------    
;内存复制 : 源地址,目标地址,字节数
;入参:
; esi = 源地址
; edi = 目标地址
; ecx = 字节数
MemCopy:
rep movsb;
ret

解析执行ELF文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

;-----------------------------------
; 解析执行ELF文件: AnalyzeELF
; 入参:
; eax=文件内存位置
; 出参:
; ebx=入口地址
AnalyzeELF:


mov edx, 0
mov ecx, 0
mov ebx, [eax + 28] ;program header偏移量
add ebx, eax ;program header位置
mov dx, [eax + 42] ;program header大小
mov cx, [eax + 44] ;program header数量


.loopSegment:
cmp byte [ebx + 0],0 ;ptype为0,程序段未使用
je .nextSegment


push ecx;
mov ecx, 0;
;---------------
;复制segment
mov esi, [ebx + 4] ;segment偏移量
add esi, eax ;src
mov edi, [ebx + 8] ;dist
mov cx, [ebx + 16] ;len
call MemCopy
pop ecx;

.nextSegment:
add ebx, edx
loop .loopSegment ;继续读取下一个segment
mov ebx, [eax + 24] ;返回入口地址
ret

调用处的代码

1
2
3
4
5
6
7
;----------------------
;解析并执行ELF文件
AnalyzeKernel:
mov eax, KERNEL_BASE_ADDR
call AnalyzeELF
jmp ebx

执行的c语言程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int is_prime(unsigned short n)
{
//返回1表示素数,返回0表示非素数
int i = 0;
for (i = 2; i < n; i++)
{
if (n % i == 0)
{
return 0;
}
}
return 1;
}

int _start(){
unsigned short* pvga = (unsigned short*)0xb8000; //填充到显示内存的初始地址
for(int i = 0;i <= 0x7fff;i++){
//char: 0x3 ,color: 0x104
if(is_prime(i) == 1) {
*(pvga + i) = (unsigned short)0x1704; //显存填充,蓝色背景白色棱形
} else {
*(pvga + i) = (unsigned short)0x1700; //显存填充背景色
}
}
fin:
goto fin;
}


Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# tools
PLATFORM=Linux
NASM=nasm
BOCHS=bochs
BXIMAGE=bximage

# args
boot=boot
build=build
ENTRY_POINT=0x10000
CFLAGS=-m32 -c -nostdinc -nostdlib -fno-builtin -Wall -W -Wstrict-prototypes -Wmissing-prototypes
LDFLAGS=-m elf_i386 -s -e _start -Ttext $(ENTRY_POINT)

target: $(build)/rastos.img
@echo "build img completed"

$(build)/rastos.img:$(build)/boot.bin $(build)/loader.bin $(build)/loaderELF.bin
$(BXIMAGE) -func=create -imgmode=flat -hd=16M -q $(build)/rastos.img
sleep 2
dd if=$(build)/boot.bin of=$(build)/rastos.img bs=512 count=1 conv=notrunc
dd if=$(build)/loader.bin of=$(build)/rastos.img bs=512 count=1 seek=1 conv=notrunc
dd if=$(build)/loaderELF.bin of=$(build)/rastos.img bs=512 count=30 seek=2 conv=notrunc


$(build)/loaderELF.bin: $(build)/loaderELF.o
$(LD) $(build)/loaderELF.o -o $(build)/loaderELF.bin $(LDFLAGS)

$(build)/loaderELF.o: $(boot)/loaderELF.c
$(CC) $(boot)/loaderELF.c -o $(build)/loaderELF.o $(CFLAGS)

$(build)/%.bin: $(boot)/%.asm
$(NASM) -f bin -o $(build)/$*.bin $(boot)/$*.asm

prepare: $(build)
@echo "prepare dir $(build)"
ifeq ($(build), $(wildcard $(build)))
@echo "build directory exist..."
else
mkdir -p $(build)
endif

clean:
@echo "clean dir $(build)"
rm -rf $(build)/*

platform:
@echo $(PLATFORM)

加载执行c语言程序的代码并执行

images/3_2_1.png

天空任鸟飞,海阔凭鱼跃。我们已经成功的从启动,到进入c语言的世界,接下来就是无限的可能性。

0%