//bootpack.c  完整代码
#include <stdio.h> void io_hlt(void);
void io_cli(void);
void io_out8(int port, int data);
int io_load_eflags(void);
void io_store_eflags(int eflags); void init_palette(void);
void set_palette(int start, int end, unsigned char *rgb);
void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1);
void init_screen8(char *vram, int x, int y);
void putfont8(char *vram, int xsize, int x, int y, char c, char *font);
void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s);
void init_mouse_cursor8(char *mouse, char bc);
void putblock8_8(char *vram, int vxsize, int pxsize,
int pysize, int px0, int py0, char *buf, int bxsize); #define COL8_000000 0
#define COL8_FF0000 1
#define COL8_00FF00 2
#define COL8_FFFF00 3
#define COL8_0000FF 4
#define COL8_FF00FF 5
#define COL8_00FFFF 6
#define COL8_FFFFFF 7
#define COL8_C6C6C6 8
#define COL8_840000 9
#define COL8_008400 10
#define COL8_848400 11
#define COL8_000084 12
#define COL8_840084 13
#define COL8_008484 14
#define COL8_848484 15 //接受启动信息写成结构体的形式 P89.接着从asmhead.nas中读取启动信息数据(启动地址和内容)
//注意在第一天中,我们已经把asmhead.nas中的信息写到了镜像中。每次系统启动时,首先载入镜像,然后读到asmhead.nas中的内容启动
struct BOOTINFO {
char cyls, leds, vmode, reserve;
short scrnx, scrny;
char *vram;
}; struct SEGMENT_DESCRIPTOR { //GDT的内容;8字节
short limit_low, base_low;
char base_mid, access_right;
char limit_high, base_high;
}; struct GATE_DESCRIPTOR { //IDT的内容,8字节
short offset_low, selector;
char dw_count, access_right;
short offset_high;
}; void init_gdtidt(void);
void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar);
void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar);
void load_gdtr(int limit, int addr);
void load_idtr(int limit, int addr); void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
char s[], mcursor[];
int mx, my; init_gdtidt();
init_palette();
init_screen8(binfo->vram, binfo->scrnx, binfo->scrny); //所谓的使用箭头记号->
mx = (binfo->scrnx - ) / ; /* 启动信息结构体BOOTINFO 从asmhead.nas中读取启动信息数据*/
my = (binfo->scrny - - ) / ; init_mouse_cursor8(mcursor, COL8_008484); //初始化并显示鼠标指针
putblock8_8(binfo->vram, binfo->scrnx, , , mx, my, mcursor, ); sprintf(s, "(%d, %d)", mx, my); //显示变量,先把内容放到字符串s中
putfonts8_asc(binfo->vram, binfo->scrnx, , , COL8_FFFFFF, s); //接着把字符串s显示出来 for (;;) {
io_hlt();
}
} void init_palette(void)
{
static unsigned char table_rgb[ * ] = {
0x00, 0x00, 0x00,
0xff, 0x00, 0x00,
0x00, 0xff, 0x00,
0xff, 0xff, 0x00,
0x00, 0x00, 0xff,
0xff, 0x00, 0xff,
0x00, 0xff, 0xff,
0xff, 0xff, 0xff,
0xc6, 0xc6, 0xc6,
0x84, 0x00, 0x00,
0x00, 0x84, 0x00,
0x84, 0x84, 0x00,
0x00, 0x00, 0x84,
0x84, 0x00, 0x84,
0x00, 0x84, 0x84,
0x84, 0x84, 0x84
};
set_palette(, , table_rgb);
return;
} void set_palette(int start, int end, unsigned char *rgb)
{
int i, eflags;
eflags = io_load_eflags();
io_cli();
io_out8(0x03c8, start);
for (i = start; i <= end; i++) {
io_out8(0x03c9, rgb[] / );
io_out8(0x03c9, rgb[] / );
io_out8(0x03c9, rgb[] / );
rgb += ;
}
io_store_eflags(eflags);
return;
} void boxfill8(unsigned char *vram, int xsize, unsigned char c, int x0, int y0, int x1, int y1)
{
int x, y;
for (y = y0; y <= y1; y++) {
for (x = x0; x <= x1; x++)
vram[y * xsize + x] = c;
}
return;
} void init_screen8(char *vram, int x, int y)
{
boxfill8(vram, x, COL8_008484, , , x - , y - );
boxfill8(vram, x, COL8_C6C6C6, , y - , x - , y - );
boxfill8(vram, x, COL8_FFFFFF, , y - , x - , y - );
boxfill8(vram, x, COL8_C6C6C6, , y - , x - , y - ); boxfill8(vram, x, COL8_FFFFFF, , y - , , y - );
boxfill8(vram, x, COL8_FFFFFF, , y - , , y - );
boxfill8(vram, x, COL8_848484, , y - , , y - );
boxfill8(vram, x, COL8_848484, , y - , , y - );
boxfill8(vram, x, COL8_000000, , y - , , y - );
boxfill8(vram, x, COL8_000000, , y - , , y - ); boxfill8(vram, x, COL8_848484, x - , y - , x - , y - );
boxfill8(vram, x, COL8_848484, x - , y - , x - , y - );
boxfill8(vram, x, COL8_FFFFFF, x - , y - , x - , y - );
boxfill8(vram, x, COL8_FFFFFF, x - , y - , x - , y - );
return;
} //输出,显示字符
void putfont8(char *vram, int xsize, int x, int y, char c, char *font)
{
int i;
char *p, d /* data */;
for (i = ; i < ; i++) {
p = vram + (y + i) * xsize + x;
d = font[i];
if ((d & 0x80) != ) { p[] = c; }
if ((d & 0x40) != ) { p[] = c; }
if ((d & 0x20) != ) { p[] = c; }
if ((d & 0x10) != ) { p[] = c; }
if ((d & 0x08) != ) { p[] = c; }
if ((d & 0x04) != ) { p[] = c; }
if ((d & 0x02) != ) { p[] = c; }
if ((d & 0x01) != ) { p[] = c; }
}
return;
} //输出,显示字符串
void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
{
extern char hankaku[];
for (; *s != 0x00; s++) {
putfont8(vram, xsize, x, y, c, hankaku + *s * );
x += ;
}
return;
} void init_mouse_cursor8(char *mouse, char bc)
/* 准备,初始化鼠标指针 16*16=512字符(字节)*/
{
static char cursor[][] = {
"**************..",
"*OOOOOOOOOOO*...",
"*OOOOOOOOOO*....",
"*OOOOOOOOO*.....",
"*OOOOOOOO*......",
"*OOOOOOO*.......",
"*OOOOOOO*.......",
"*OOOOOOOO*......",
"*OOOO**OOO*.....",
"*OOO*..*OOO*....",
"*OO*....*OOO*...",
"*O*......*OOO*..",
"**........*OOO*.",
"*..........*OOO*",
"............*OO*",
".............***"
};
int x, y; for (y = ; y < ; y++) {
for (x = ; x < ; x++) {
if (cursor[y][x] == '*') {
mouse[y * + x] = COL8_000000;
}
if (cursor[y][x] == 'O') {
mouse[y * + x] = COL8_FFFFFF;
}
if (cursor[y][x] == '.') {
mouse[y * + x] = bc;
}
}
}
return;
}
//这里显示鼠标指针;
void putblock8_8(char *vram, int vxsize, int pxsize,
int pysize, int px0, int py0, char *buf, int bxsize)
{
int x, y;
for (y = ; y < pysize; y++) {
for (x = ; x < pxsize; x++) {
vram[(py0 + y) * vxsize + (px0 + x)] = buf[y * bxsize + x];
}
}
return;
} void init_gdtidt(void) //GDT和IDT的初始化
{
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000;
struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) 0x0026f800;
int i; /* GDT初始化 */
for (i = ; i < ; i++) { //i每次加一;但是gdt指向8字节的结构体;结果地址增加了8
set_segmdesc(gdt + i, , , ); //gdt*(地址)每次+8
}
set_segmdesc(gdt + , 0xffffffff, 0x00000000, 0x4092); //段号为1;大小4G 表示CPU管理的全部内存
set_segmdesc(gdt + , 0x0007ffff, 0x00280000, 0x409a); //段号位2; 大小512K 为bootpack.hrb准备
load_gdtr(0xffff, 0x00270000); //GDT 0x270000-0x27ffff 借助汇编语言的力量给寄存器GDTR赋值 /* IDT初始化 */
for (i = ; i < ; i++) { //和上面一样
set_gatedesc(idt + i, , , );
} //IDT 0x26f800-0x26ffff
load_idtr(0x7ff, 0x0026f800); //向寄存器IDTR赋值 return;
} void set_segmdesc(struct SEGMENT_DESCRIPTOR *sd, unsigned int limit, int base, int ar)
{
if (limit > 0xfffff) {
ar |= 0x8000; /* G_bit = 1 */
limit /= 0x1000;
}
sd->limit_low = limit & 0xffff;
sd->base_low = base & 0xffff;
sd->base_mid = (base >> ) & 0xff;
sd->access_right = ar & 0xff;
sd->limit_high = ((limit >> ) & 0x0f) | ((ar >> ) & 0xf0);
sd->base_high = (base >> ) & 0xff;
return;
} void set_gatedesc(struct GATE_DESCRIPTOR *gd, int offset, int selector, int ar)
{
gd->offset_low = offset & 0xffff;
gd->selector = selector;
gd->dw_count = (ar >> ) & 0xff;
gd->access_right = ar & 0xff;
gd->offset_high = (offset >> ) & 0xffff;
return;
}

bootpack.c 完整代码day_05

harib02a:

  P89 这里做的就是数值写入asmhead.nas中,然后取值;
  而不是将这些数值直接写入程序bootpack.c中

//bootpack.c节选
void HariMain(void)
{
char *vram;
int xsize, ysize;
short *binfo_scrnx, *binfo_scrny;
int *binfo_vram;
init_palette();
binfo_scrnx = (short *) 0x0ff4;
binfo_scrny = (short *) 0x0ff6;
binfo_vram = (int *) 0x0ff8;
xsize = *binfo_scrnx;
ysize = *binfo_scrny;
vram = (char *) *binfo_vram; init_screen(vram, xsize, ysize);
for (;;) {
io_hlt();
}
}

harib02b:
  这次使用的是结构体的方法重新写一遍,实现的内容是一样的
  只是实现方法在这里使用了结构体的写法。如下:

//bootpack.c节选
//接受启动信息写成结构体的形式 P89.接着从asmhead.nas中读取启动信息数据(启动地址和内容)
//注意在第一天中,我们已经把asmhead.nas中的信息写到了镜像中。每次系统启动时,首先载入镜像,然后读到asmhead.nas中的内容启动
struct BOOTINFO {
char cyls, leds, vmode, reserve;
short scrnx, scrny;
char *vram;
};
void HariMain(void)
{
char *vram;
int xsize, ysize;
struct BOOTINFO *binfo; init_palette();
binfo = (struct BOOTINFO *) 0x0ff0;
xsize = (*binfo).scrnx;
ysize = (*binfo).scrny;
vram = (*binfo).vram; init_screen(vram, xsize, ysize); for (;;) {
io_hlt();
}
}

harib02c:
  这一步在结构体的基础上,引入了箭头记号的C变成的方式
  eg::xsize = (*binfo).scrnx  可以写成  xsize = binfo -> scrnx
  进一步改进了程序,但实现的内容任然不变

void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0; init_palette();
init_screen(binfo->vram, binfo->scrnx, binfo->scrny); for (;;) {
io_hlt();
}
}

harib02d:
  P92 显示字符;
  原理:字符使用8*16的像素点阵来表示,
  1个字符是16个字节
  建立了font结构体数组,用来表示像素点阵字符
  函数purfont8()用来将像素点阵字符输出

//字符显示函数
void putfont8(char *vram, int xsize, int x, int y, char c, char *font)
{
int i;
char *p, d /* data */;
for (i = ; i < ; i++) {
p = vram + (y + i) * xsize + x;
d = font[i];
if ((d & 0x80) != ) { p[] = c; }
if ((d & 0x40) != ) { p[] = c; }
if ((d & 0x20) != ) { p[] = c; }
if ((d & 0x10) != ) { p[] = c; }
if ((d & 0x08) != ) { p[] = c; }
if ((d & 0x04) != ) { p[] = c; }
if ((d & 0x02) != ) { p[] = c; }
if ((d & 0x01) != ) { p[] = c; }
}
return;
}

harib02e:
  P94 增加了字体的显示; 这里使用了OSASK的字体数据;
  原理:相应了字体显示的像素点阵存储在hankaku.txt中。
  每一个字体字符都有一个编号。直接查找调用即可
  方法:用OSASK的专用编译器makefont.exe将hankaku.txt编译成hankaku.bin
  再由连接器bin2obj.exer将hankaku.bin加上必须的接口信息和bootpack.obj链接
  生成目标文件

//显示ABC  123
void HariMain(void)
{
struct BOOTINFO *binfo = (struct BOOTINFO *) 0x0ff0;
extern char hankaku[]; init_palette();
init_screen(binfo->vram, binfo->scrnx, binfo->scrny);
putfont8(binfo->vram, binfo->scrnx, , , COL8_FFFFFF, hankaku + 'A' * );
putfont8(binfo->vram, binfo->scrnx, , , COL8_FFFFFF, hankaku + 'B' * );
putfont8(binfo->vram, binfo->scrnx, , , COL8_FFFFFF, hankaku + 'C' * );
putfont8(binfo->vram, binfo->scrnx, , , COL8_FFFFFF, hankaku + '' * );
putfont8(binfo->vram, binfo->scrnx, , , COL8_FFFFFF, hankaku + '' * );
putfont8(binfo->vram, binfo->scrnx, , , COL8_FFFFFF, hankaku + '' * );
for (;;) {
io_hlt();
}
}

harib02f:
  接着笔者干脆写了一个函数用来专门显示字符串;putfonts8_asc();

//字符串显示函数
void putfonts8_asc(char *vram, int xsize, int x, int y, char c, unsigned char *s)
{
extern char hankaku[];
for (; *s != 0x00; s++) {
putfont8(vram, xsize, x, y, c, hankaku + *s * );
x += ;
}
return;
}

harib02g:
  P98 显示变量的值;虽然我们不能用prinft()函数;但是我们可以使用sprintf()函数
  sprintf()将输出的内容写在内存中间。这个函数只对内存进行操作,可以应用于所有的操作系统
  笔者还附带的介绍了一下sprintf()的使用方法和格式;

    sprintf(s, "scrnx = %d", binfo->scrnx);
putfonts8_asc(binfo->vram, binfo->scrnx, , , COL8_FFFFFF, s);

harib02h:
  P100 鼠标指针的显示。大小设定为16*16的字符数组的大小
  内存空间:16*16=256字节;程序写在了init_mouse_cursor8中。
  显示的原理和上面字符显示的原理一样;将buf中的数据复制到VARM中去就可以了
  接下来笔者写了一个显示的函数putblock8_8();教材100面有函数详细的介绍。

//bootpack.c 节选
void init_mouse_cursor8(char *mouse, char bc)
/* 准备,初始化鼠标指针 16*16=512字符(字节)*/
{
static char cursor[][] = {
"**************..",
"*OOOOOOOOOOO*...",
"*OOOOOOOOOO*....",
"*OOOOOOOOO*.....",
"*OOOOOOOO*......",
"*OOOOOOO*.......",
"*OOOOOOO*.......",
"*OOOOOOOO*......",
"*OOOO**OOO*.....",
"*OOO*..*OOO*....",
"*OO*....*OOO*...",
"*O*......*OOO*..",
"**........*OOO*.",
"*..........*OOO*",
"............*OO*",
".............***"
};
int x, y; for (y = ; y < ; y++) {
for (x = ; x < ; x++) {
if (cursor[y][x] == '*') {
mouse[y * + x] = COL8_000000;
}
if (cursor[y][x] == 'O') {
mouse[y * + x] = COL8_FFFFFF;
}
if (cursor[y][x] == '.') {
mouse[y * + x] = bc;
}
}
}
return;
}
//这里显示鼠标指针;
void putblock8_8(char *vram, int vxsize, int pxsize,
int pysize, int px0, int py0, char *buf, int bxsize)
{
int x, y;
for (y = ; y < pysize; y++) {
for (x = ; x < pxsize; x++) {
vram[(py0 + y) * vxsize + (px0 + x)] = buf[y * bxsize + x];
}
}
return;
}

harib02i:
  P101 怎么才能让鼠标动起来?我们先来看看什么事GDT和IDT
  这两个东西是CPU有关的设定(大家暂时不用深究什么是设定,先往下看)
  接下来要用汇编语言(有回到汇编语言了)来对CPU做一些鼠标的设定
  接下来笔者讲了操作系统分段的概念,关于分段、分页。学过操作系统的都应该知道,不再赘述
  GDT:全局段号记录表,存放在内存中的。把需要查找的地址和相应的段号对应起来,便于寻址和查找;
      寄存器GDTR用来存储该段内存的起始地址和有效的设定个数
  IDT:中断记录表;中断号为0-255;每一个中断号对应一个函数调用,
       这些函数就是用来处理操作系统中的中断的,中断发生后,调用通过IDT调用相应的中断函数即可

//bootpack.c节选
struct SEGMENT_DESCRIPTOR { //GDT的内容;8字节
short limit_low, base_low;
char base_mid, access_right;
char limit_high, base_high;
}; struct GATE_DESCRIPTOR { //IDT的内容,8字节
short offset_low, selector;
char dw_count, access_right;
short offset_high;
};
...................
void init_gdtidt(void) //GDT和IDT的初始化
{
struct SEGMENT_DESCRIPTOR *gdt = (struct SEGMENT_DESCRIPTOR *) 0x00270000;
struct GATE_DESCRIPTOR *idt = (struct GATE_DESCRIPTOR *) 0x0026f800;
int i; /* GDT初始化 */
for (i = ; i < ; i++) { //i每次加一;但是gdt指向8字节的结构体;结果地址增加了8
set_segmdesc(gdt + i, , , ); //gdt*(地址)每次+8
}
set_segmdesc(gdt + , 0xffffffff, 0x00000000, 0x4092); //段号为1;大小4G 表示CPU管理的全部内存
set_segmdesc(gdt + , 0x0007ffff, 0x00280000, 0x409a); //段号位2; 大小512K 为bootpack.hrb准备
load_gdtr(0xffff, 0x00270000); //GDT 0x270000-0x27ffff 借助汇编语言的力量给寄存器GDTR赋值 /* IDT初始化 */
for (i = ; i < ; i++) { //和上面一样
set_gatedesc(idt + i, , , );
} //IDT 0x26f800-0x26ffff
load_idtr(0x7ff, 0x0026f800); //向寄存器IDTR赋值 return;
}

《30天自制操作系统》05_day_学习笔记的更多相关文章

  1. 《30天自制操作系统》学习笔记--Mac下工具的使用

    现在来介绍官网上下的工具怎么用首先是官网地址,书上有个注释上有:hrb.osask.jp 翻译成中文大概是这个样子滴. 上面有两个文件可以下载,一个是工具,一个是工具的源代码,很好的学习资料 下面把工 ...

  2. 《30天自制操作系统》学习笔记--Mac环境搭建

    弄了三天了,终于弄好了,先说结果,就是作者在网站上放了os x的工具(hrb.osask.jp,也有linux下的工具,可以自己去下载),也就是说我白忙活了三天... 再说一下这几天都干啥了,主要是想 ...

  3. 《30天自制操作系统》学习笔记--番外篇之Mac环境下的工具介绍

    这几天又有点不务正业了,书也没看,一直在搞这个破环境,尝试各种做法,网上各种垃圾信息,浪费了很多时间,说的基本都是废话,不过还是找到了一些,赶紧写下来,不然这个过几天又忘了 首先是环境,我用的是Max ...

  4. 《30天自制操作系统》读书笔记(5) GDT&IDT

    梳理项目结构 项目做到现在, 前头的好多东西都忘了, 还是通过Makefile重新理解一下整个项目是如何编译的: 现在我们拥有这么9个文件: ipl10.nas    InitialProgramLo ...

  5. 《30天自制操作系统》读书笔记(3) 引入C语言

    这一次的学习相当曲折, 主要是因为粗心, Makefile里面的错误导致了文件生成出现各种奇奇怪怪的问题, 弄得心力交瘁, 因此制作过程还是尽量按着作者的路子来吧. 作者提供的源码的注释在中文系统下是 ...

  6. 《30天自制操作系统》读书笔记(2)hello, world

    让系统跑起来 要写一个操作系统,我们首先要有一个储存系统的介质,原版书似乎是06年出版的,可惜那时候没有电脑,没想到作者用的还是软盘,现在的电脑谁有软驱?不得已我使用一张128M的SD卡来代替,而事实 ...

  7. 30天自制操作系统第九天学习笔记(u盘软盘双启动版本)

    暑假学习小日本的那本书:30天自制操作系统 qq交流群:122358078    ,更多学习中的问题.资料,群里分享 environment:开发环境:ubuntu 第九天的课程已学完,确实有点不想写 ...

  8. 从你的u盘启动:30天自制操作系统第四天u盘启动学习笔记

    暑假学习小日本的那本书:30天自制操作系统 qq交流群:122358078    ,更多学习中的问题.资料,群里分享 developing environment:ubuntu 关于u盘启动自己做的操 ...

  9. 30天自制操作系统第八天学习笔记(u盘软盘双启动版本)

    暑假学习小日本的那本书:30天自制操作系统 qq交流群:122358078    ,更多学习中的问题.资料,群里分享 environment:开发环境:ubuntu 第八天的学习思考: 关于鼠标是怎么 ...

  10. 《30天自制操作系统》笔记(03)——使用Vmware

    <30天自制操作系统>笔记(03)——使用Vmware 进度回顾 在上一篇,实现了用IPL加载OS程序到内存,然后JMP到OS程序这一功能:并且总结出下一步的OS开发结构.但是遇到了真机测 ...

随机推荐

  1. 异步调试神器Slog,“从此告别看日志,清日志文件了”

    微信调试.API调试和AJAX的调试的工具,能将日志通过WebSocket输出到Chrome浏览器的console中  — Edit 92 commits 4 branches 3 releases ...

  2. cPanel设置自定义404错误页

    利用这个cpanel的错误页工具,你就可以定制错误页面了.设置自定义404错误页,有两种简单的方法. 一,利用cpanel后台控制面板添加设置404自定义错误页的方法 步骤       1.登录cPa ...

  3. ECharts SSH+JQueryAjax+Json+JSP将数据库中数据填充到ECharts中

    本文引用自:http://blog.csdn.net/ArcticFoxHan/article/details/38071641   1.导入包,搭建SSH框架 导入Jquery的JS包,<sc ...

  4. WCF TCP 错误代码 10061: 由于目标计算机积极拒绝

    表象是连不上服务端,本质原因多种多样,网络硬件问题导致的网络不通.服务本身问题或没有启动.或者防火墙阻隔等等不一而足. 1.ping看服务端能否ping通: 2.telnet ip地址 端口 ,看看是 ...

  5. .Net程序员安卓学习之路6:等待条

    一般在需要访问网络或者长时间操作的时候避免界面无响应才使用:等待条 本例将实现一个无框架的等待条,效果如下: 点击后,使线程Sleep5秒,就出现如下效果: 实现代码如: private Progre ...

  6. Introduction to Project Management(II)

    Introduction The purpose of this paper is to gain an understanding of project management and to give ...

  7. SQL学习笔记 SQL ORDER BY 关键字

    SELECT column_name,column_nameFROM table_nameORDER BY column_name,column_name ASC|DESC; SELECT id, n ...

  8. android apk简单反编译

    1. 查看或者提取资源文件:     使用谷歌官方工具apktool,命令行如下: apktool d xxx.apk xxx_decode d代表反编译,xxx.apk为你要反编译的apk,xxx_ ...

  9. C++经典编程题#1:含k个3的数

    总时间限制:  1000ms 内存限制:  65536kB 描述 输入两个正整数 m 和 k,其中1 < m < 100000,1 < k < 5 ,判断 m 能否被19整除, ...

  10. synchronized原理

    http://www.cnblogs.com/YDDMAX/p/5658607.html http://www.cnblogs.com/YDDMAX/p/5658668.html synzhroniz ...