首页
社区
课程
招聘
[分享]学写操作系统(一)……(十五)
发表于: 2010-12-30 01:27 19055

[分享]学写操作系统(一)……(十五)

2010-12-30 01:27
19055
收藏
免费 7
支持
分享
最新回复 (77)
雪    币: 1099
活跃值: (100)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
51
恭喜啊!这样的写出来很给我们新手很有用!谢谢分享
2011-1-3 22:36
0
雪    币: 81
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
52
系统堆栈的建立:
我们以后要不停的用堆栈,为了防止发生的可能的冲突,建一个堆栈是一个当务之急。
当bios启动会有一个堆栈,默认的栈底bp为0,sp当跳入系统时为ffd6,用这个堆栈暂时也可以,但是为了保险起见,我们新开一个堆栈。由于现在暂时还不用多个堆栈的切换,我们趁进程未进入视野时,现看看堆栈怎么切换,为简便起见,
这次不用很多编程,只是将初始堆栈设为
mov esp,0x200000
mov ebp,0x200000
这两条语句加载在伟大一跳(jmp dword CS_SELECTOR:kernel_start+EntryPoint)之前。
然后在bochs利用命令切换。
bohcs指令
b 0x7e08
c
r

好了,
rsp: 0x00000000:00200000 rbp: 0x00000000:00200000
我们的堆栈现在进入指定的范围,一个空堆栈什么都没有,
u 0x7ef8 0x7f10
下两条语句是
00007ef8: (                    ) push ebp                  ; 55
00007ef9: (                    ) mov ebp, esp              ; 89e5
00007efb: (                    ) sub esp, 0x00000010       ; 83ec10
s
r
单步执行看看堆栈
rsp: 0x00000000:001ffffc rbp: 0x00000000:00200000
明显栈底没变,栈顶减4,0x00200000-4=0x001ffffc
下一条语句,切换栈底
s
r
变为rsp: 0x00000000:001ffffc rbp: 0x00000000:001ffffc
栈底变了,我们怎么知道栈底在那里呢,已经存进栈了,只要记住我们的最后的真正系统栈底就不会犯错了,下一句
s
r
实际上就是栈顶减16,栈是向下增长的
rsp: 0x00000000:001fffec rbp: 0x00000000:001ffffc
打开计算器,0x001ffffc-0x10=001fffec
okay,栈没错了,
现在验证我们存的系统栈底在那里,用bochs看看,
应该是现在的栈底指向位置的dword,
<bochs:12> x /4b 0x1ffffc
出现
[bochs]:0x00000000001ffffc <bogus+       0>    0x00    0x00    0x20    0x00
okay,确实存进去了,
现在怀疑ebp就是个摆设,系统不会管的,为验证这一点,让栈底跟栈顶错乱,看看效果,
先定位到下一条操作堆栈语句,
再敲一个s
s
看见了下一组堆栈操作语句
00007e00: (                    ) push ebp                  ; 55
00007e01: (                    ) mov ebp, esp              ; 89e5
00007e03: (                    ) sub esp, 0x00000010       ; 83ec10
机会来了,是我们大显身手的时候,现在什么操作系统都没有,想干啥就干啥,反正我们设得权限是无穷的,想怎么写就怎么写,我们是操作系统,
我们将栈顶和栈底干脆调各个,看看你牛还是我牛
bochs键入
set rsp=0x001ffffc
set rbp=0x001fffe8
r
现在看看,
rsp: 0x00000000:001ffffc rbp: 0x00000000:001fffe8
噻,什么人吗,被intel忽悠了,ebp就是聋子的耳朵,摆设,其实就是一个普通寄存器而已,
大家都被忽悠了!
下面
s
r
语句push ebp效果仍然是我行我素,跟栈底没任何关系,
rsp: 0x00000000:001ffff8 rbp: 0x00000000:001fffe8
大家现在知道了吧,push,pop指令只是操作栈顶而已,实际上64位cpu时,amd变乖了,amd64没有栈底了,还是amd老实啊,没有就是没有。纠正一点x64指amd64,intel的IA64估计只有他会了。
呵呵,切换堆栈实际上很轻松吧,我们没费吹灰之力就切换了!
注:我用的是64位cpu,权当32位看好了,rbp对应ebp等等
未完待续
上传的附件:
2011-1-4 02:07
0
雪    币: 78
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
53
贵在坚持,楼主加油!
2011-1-4 06:38
0
雪    币: 367
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
54
楼主精神可嘉,值得我学习!
共同进步!
2011-1-4 15:16
0
雪    币: 2477
活跃值: (20)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
55
等待楼主更精彩的文章
2011-1-6 21:25
0
雪    币: 81
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
56
貌似简单的LDT:
老实说LDT除了有点绕以外,没有特别的,但是,就是这个简单的不能再简单的问题,折磨了我两天,所有的方法都用了,参考了intel保护模式手册,linux内核,minix内核,simplix操作系统,自己动手写操作系统,网络操作系统实验,把所有的无关的代码全去掉了,然后将汇编读了n遍,最后总是在使用ldt选择子时,操作系统当掉了,看着bios重新加载,简直欲哭无泪。最后找到一哥们一段不显眼的代码,发现原因了,终于我们的LDT成熟了,我们的LDT选择子,起作用了。以下尽量一一道来。
为什么要用LDT呢,实际上GDT只有一个,要实现进程和线程用LDT是最合适不过了,进程只要切换LLDT,线程只要切换段选择子是在合适不过了。
GDT和LDT很像,但说实在的真不是一个东西。
去掉浮云一样的多如牛毛的解释,gdt就是一个数组,lgdt就是gdt的指针,但就像c语言一样,数组需要基地址,元素大小,元素个数,访问时需要索引,gdt大小都是64位的,大小知道了,所以需要知道基地址和个数,所以实际上gdt存的是带大小的指针,而选择子就是索引,第一个保留,真正的第一个就是8,第二个就是16,第三个就是24,......,你仔细看就会发现最后三比特位0,然后是1,2,3
即1000b,10000b,11000b
ldtr就是索引,由ldtr和gdt(偏移和指针)定位到该段,找到该段基址,该段大小在界限里,这不是相当于gdt指针又定位到一个数组吗!所以其实两者很像!而索引也是如上所述定义,那么假如我jmp 4:1,你说我到底是去哪找呢,gdt数组还是ldt数组,所以后三位就存在着标志,gdt后三位为0,ldt后三位为4,其实都是浮云。所以ldt索引是0+4,8+4,16+4,当然ldt没有保留数组第一项,所以从0开始。
代码如下:
boot端
LoadSectorIndex                equ                02
  LoadSectorTotal         equ         1
  LoadAddress                                equ                7e00h
  EntryPoint                                equ                0;12dh;00f8h;148h;
  
  DA_C                EQU        98h        ; 存在的只执行代码段属性值
  DA_LDT                EQU          82h        ; 局部描述符表段类型值
  DA_32                EQU        4000h        ; 32 位段
  DA_DRW                EQU        92h        ; 存在的可读写数据段属性值
  %macro Descriptor 3
        dw        %2 & 0FFFFh                                ; 段界限 1                                (2 字节)
        dw        %1 & 0FFFFh                                ; 段基址 1                                (2 字节)
        db        (%1 >> 16) & 0FFh                        ; 段基址 2                                (1 字节)
        dw        ((%2 >> 8) & 0F00h) | (%3 & 0F0FFh)        ; 属性 1 + 段界限 2 + 属性 2                (2 字节)
        db        (%1 >> 24) & 0FFh                        ; 段基址 3                                (1 字节)
%endmacro ;
  
  org        7c00h
  mov ax,0
  mov es,ax
  mov bx,LoadAddress
  mov ah,2
  mov al,LoadSectorTotal        ;加载扇区数
  mov ch,0
  mov cl,LoadSectorIndex
  mov dh,0
  mov dl,0
  int 13h        ;将磁盘第LoadSectorIndex扇区开始LoadSectorTotal个扇区装入内存LoadAddress
  jmp start   
  
  ;定义GDT
        GDT_ADDRESS:
        dw         0,0,0,0                                                        ;默认
        CS_ADDRESS:
        dw 0xFFFF,0,0x9A00,0x00CF                        ;代码段,大小4G
        dw 0xFFFF,0,0x9200,0x00CF                 ;数据段, 大小4g
        LABEL_DESC_LDT:                Descriptor               0,        7, DA_LDT                ; LDT
        GDT_LEN                equ $-GDT_ADDRESS
       
        ;SelectorLDT                equ        LABEL_DESC_LDT                - GDT_ADDRESS
       
        ;GDTR LOAD       
        GDT_LOAD:
        dw        GDT_LEN       
        dw        0,0        ;compute as follow
       
        CS_SELECTOR        equ CS_ADDRESS-GDT_ADDRESS       
       
        ;IDTR LOAD        中断向量
        IDT_LOAD:
        dw        0
        dw        0,0       
       
start:
        ;设置GDT基址
        xor eax,eax                                                        ;清零
        ;compute ds:GDT_ADDRESS
        mov ax,ds                                                               
        shl eax,4                                                                ;段地址乘以16
        add eax,GDT_ADDRESS
        mov dword [GDT_LOAD+2],eax                                ;装入基地址
        lgdt [GDT_LOAD]                                                ;加载到GDTR寄存器
       
       
        ;初始化新的中断向量表
        lidt [IDT_LOAD]
       
        ;屏蔽所有可屏蔽中断-关掉所有8259中断
        mov al,0xff        ;mask all interrupts for now
        out 0xA1,al
        out 0x80,al        ;Delay is needed after doing I/O
       

        ; 保证所有的协处理都被正确的Reset
        xor ax, ax
        out 0xf0,al
        out 0x80,al        ;Delay is needed after doing I/O

        out 0xf1,al
        out 0x80,al        ;Delay is needed after doing I/O       
          
        ;进入32位模式        ;//CR0寄存器的PE位
        mov ax,1
        LMSW  ax
       
        ;设置段为第二项-数据段
        mov ax,0x10
  mov ds,ax  
  mov es,ax  
  mov fs,ax
  mov gs,ax  
  mov ss,ax   
        mov esp,0x200000
        mov ebp,0x200000
       
                jmp  CS_SELECTOR:kernel_start+EntryPoint
        dw 0       
       
times         510-($-$$)        db        0        ; 填充剩下的空间,使生成的二进制代码恰好为512字节
dw         0xaa55                                ; 结束标志
kernel_start:                ;占位符,会放置c语言编译后的代码
        db 0

代码没什么稀奇的,需要说明的是
LABEL_DESC_LDT:                Descriptor               0,        7, DA_LDT                ; LDT
加一个ldt代码段,从0开始,0(非段基址的0,而是首地址),1,2,3,4,5,6,7加起来正好是8个字节,
我们准备覆盖中断向量表,没啥的,我们不用现在。
好了c端代码,
typedef unsigned short uint16_t;
typedef unsigned char uint8_t;
#pragma pack(1)
struct Segment_Descriptor
{
        uint16_t         limit1;
        uint16_t         base1;
        uint8_t                base2;
        uint16_t        typelimit2;
        uint8_t                base3;       
};
#define SA_TIL        4
#define lldt(ldtr)        \
        asm("lldt %0" : : "r" (ldtr))

#define DA_CR                0x9A       
#define DA_C                0x98        //
#define DA_32                0x4000        // 32 位段
#define DA_DRW                0x92        // rw data section

void entry_os_start();
void entry_os()
{
        entry_os_start();       
}

#define BASE_ADDRERSS                0xb8000
#define LIMIT_ADDRESS                0x0ffff
#define DESCRIPTOR_TYPE         (DA_C + DA_32)

void set_gssec()
{
        uint16_t selector;
       
        struct Segment_Descriptor* pLTDS=(struct Segment_Descriptor*)0;

        pLTDS->limit1= LIMIT_ADDRESS & 0xffff;
        pLTDS->base1 = BASE_ADDRERSS & 0xffff;                            
    pLTDS->base2 = (BASE_ADDRERSS >> 16) & 0xff;                                
    pLTDS->typelimit2 =((LIMIT_ADDRESS>>8)&0xf00)|(DESCRIPTOR_TYPE & 0xf0ff);                  
    pLTDS->base3 = ((BASE_ADDRERSS) >> 24) & 0xff;
        selector=sizeof(struct Segment_Descriptor);
        selector=24;
        lldt(selector);
        asm("ljmp $4,$0" );
}  

void entry_os_start()
{
        set_gssec();
        for(;;){}
}
这里去掉了显示环节,需要说明几点,
我们将entry_os移到开头,然后跳转到真正的entry,我们不想老去idapro哪里去找跳转地址,所以一般情况下直接跳转到头部就okay了,如果bochs显示的汇编不对,再修改不迟。
SA_TIL就是区别ldt和gdt标志
asm("lldt %0" : : "r" (ldtr))摘自minix源码,
即相当于(实际上gcc中间又多分配了一个局部变量)
mov ax,ldtr
lldt ax
注意这里r就是指任选一寄存器作为中间变量(由编译器选)
asm("ljmp $4,$0" );
即jmp 4:0
实际上cpu一看到这条指令,他就知道是ldt,因为gdt的索引尾巴都是000b,然后根据gdtr和ldtr得到具体地址。
最后就要指出花了两天得来的
#pragma pack(1)
本来没有这句的,然后装入bochs运行,总是错误,查了n遍intel手册也没有找到为什么会异常,终于找到一哥们这样写gdt
struct DESCRIPTOR         //共 8 个字节
{
  t_16  limit_low;        //Limit 2字节
  t_16  base_low;         //Base  2字节
  t_8   base_mid;         //Base  1字节
  t_8   attr1;            //P(1) DPL(2) DT(1) TYPE(4)  1字节
  t_8   limit_high_attr2; //G(1) D(1) 0(1) AVL(1) LimitHigh(4) 1字节
  t_8   base_high;        //Base  1字节
};
我就纳闷了,为什么这么写呢?百无聊赖间想会不会是对齐,
还真是!
uint16_t         limit1;
        uint16_t         base1;
        uint8_t                base2;
        uint16_t        typelimit2;
        uint8_t                base3;       
走到base2时,vc和gcc统统2字节对齐,谁让咱就开始没设好编译器呢,

selector=sizeof(struct Segment_Descriptor);
是后加的,结果显示为0xa,即10,肯定不对,应为8
所以加上了
#pragma pack(1)
告诉编译器struct一字节对齐,okay,跳了,终于,跳转了。
(0) [0x000b8000] 0004:0000000000000000 (unk. ctxt): push eax
跳到了段地址首地址上0x000b8000
未完待续
上传的附件:
2011-1-6 23:00
0
雪    币: 3836
活跃值: (4142)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
57
顶顶
2011-1-6 23:20
0
雪    币: 1
活跃值: (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
58
还可以写操作系统啊
看雪的高手还真是多啊
2011-1-7 08:58
0
雪    币: 81
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
59
微内核与单内核
现在操作系统的设计分为单内核和微内核。

单内核(分层的内核):因为所有的模块都在同一个内核空间上运行,单内核结构就可以从运行效率上得到好处。

微内核(英文中常译作micro kernel)。是一种能够提供必要服务的操作系统内核;其中这些必要的服务包括任务,线程,交互进程通信(IPC,Inter-Process Communication)以及内存管理等等。所有服务(包括设备驱动)在用户模式下运行,而处理这些服务同处理其他的任何一个程序一样。因为每个服务只是在自己的地址空间运行。所以这些服务之间彼此之间都受到了保护。

从可插拔的角度讲,微内核无疑是占有绝对的优势的,而且微内核大部分系统服务都运行在用户态例如ring3级,所以内核可以足够小甚至只有一张软盘大小,需要加载的服务可以定制、可以下载,可以动态编译等等,没有你做不到的,只有你想不到的。而随着系统的复杂性逐渐增大,内核复杂性逐渐提高,逐渐远离了人能控制的程度,可插拔将无可置疑的成为系统的标配属性,由单内核特点,可插拔也许只是一个梦。随着时间推移,系统复杂度的大幅提高,单内核移植到新的架构的难度系数会呈指数级提高,虽然现在单内核的linux已经移植到绝大多数的系统上。可以预见在很远的将来,随着系统可利用资源的大幅提高,降低复杂度将会成为计算机行业的革命,那时,单内核或许就会跟大多数操作系统说再见了!
所以,我们的系统将基于微内核设计。
刚看了minix的进程的实现原理,效率实在是低啊,底层整那么费劲干嘛?车走车路,马走马路。用户态切换本身就已经是负担了,还要增加负担,何苦呢?
未完待续
上传的附件:
2011-1-8 14:10
0
雪    币: 73
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
60
楼主厉害!系统好写吗?
2011-1-8 15:54
0
雪    币: 558
活跃值: (46)
能力值: ( LV2,RANK:16 )
在线值:
发帖
回帖
粉丝
61
什么时候出个chm版本的就好了。。。方便下载
2011-1-8 16:03
0
雪    币: 81
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
62
呵呵,等系列写完了,所有文章、源码跟工具打包下载!
2011-1-8 16:09
0
雪    币: 81
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
63
很不好写,要是好写,就没有微软了!
2011-1-8 23:50
0
雪    币: 81
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
64
为了更好地理解保护模式编程,参考下linux作者linus写的非常牛的例子(boot.s和header.s)
在linux内核完全剖析0.12中第4章第九节中有详细讲解。
header.s会在以后的几节详细讲解。
nasm重写boot.s
代码如下:

BOOTSEG equ 0x07c0
SYSSEG  equ 0x1000                        ; system loaded at 0x10000 (65536).
SYSLEN  equ 17                                ; sectors occupied.
        
;entry start
start:
        jmp dword BOOTSEG:go
go:       
        mov        ax,cs
        mov        ds,ax
        mov        ss,ax
        mov        sp,0x400                ; arbitrary value >>512

load_system:
        mov        dx,0x0000
        mov        cx,0x0002
        mov        ax,SYSSEG
        mov        es,ax
        xor        bx,bx
        mov        ax,0x200+SYSLEN
        int         0x13
        jnc        ok_load
die:       
        jmp        die

ok_load:
        cli                        ; no interrupts allowed !
        mov        ax, SYSSEG
        mov        ds, ax
        xor        ax, ax
        mov        es, ax
        mov        cx, 0x2000
        cld
        rep        movsb
        mov        ax, BOOTSEG
        mov        ds, ax
        lidt        [idt_48]                ; load idt with 0,0
        lgdt        [gdt_48]                ; load gdt with whatever appropriate

; absolute address 0x00000, in 32-bit protected mode.
        mov        ax,0x0001        ; protected mode (PE) bit
        lmsw        ax                ; This is it!
        jmp dword 0x8:0        ;jmp offset 0 of segment 8 (cs)

gdt:       
        dw        0,0,0,0                ; dummy

        dw        0x07FF                ; 8Mb - limit=2047 (2048*4096=8Mb)
        dw        0x0000                ; base address=0x00000
        dw        0x9A00                ; code read/exec
        dw        0x00C0                ; granularity=4096, 386

        dw        0x07FF                ; 8Mb - limit=2047 (2048*4096=8Mb)
        dw        0x0000                ; base address=0x00000
        dw        0x9200                ; data read/write
        dw        0x00C0                ; granularity=4096, 386

idt_48:
        dw        0                ; idt limit=0
        dw        0,0                ; idt base=0L
gdt_48:
        dw        0x7ff                ; gdt limit=2048, 256 GDT entries
        dw        0x7c00+gdt,0        ; gdt base = 07xxx
       
        times 510-($-$$) db 0
        db 0x55,0xaa

需要说明的是不需要org        7c00h,
这样编译器以0地址编译
因为第一句直接跳转0x7c0段0x8处即go处,请大家慢慢体会其中的妙处。
以0编译,然后跳转到首地址为0的段,okay,一切都对上号了
加载17扇,一扇是512byte,除去第一扇,所以内核代码不能超过512*16byte=1024*8byte,即8k
所以移动8k次byte,即0x2000次,mov        cx, 0x2000含义
cld 即地址按增量处理
rep        movsb用bochs反编译即rep movsb byte ptr es:[di], byte ptr ds:[si]
即批量复制,共0x2000字节
其它我们全讲过了
哈哈,我们终于找到了linus的bug
读扇区应该是16,因为是从第二扇区读,这么牛的人应该不会错,为了保险起见,自己改了img文件,修改
16扇区前后的8字节,然后去bochs里去比对,发现确实读了17扇区,而header最多只允许16扇区,所以显然是错误的,只是复制到0处时,没有再复制而已,不影响使用
所以最终的版本应该修改
SYSLEN  equ 17       

SYSLEN  equ 16
不是牛人总是对的,
肯定会有人问为什么不直接load到0,呵呵,这是跟linux内核的boot保持一致,为了编码方便移过去的。
未完待续
2011-1-9 03:00
0
雪    币: 81
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
65
定时器8253:
8253有三个计数器,
0-2
有六个工作方式
1.方式0:计数结束则中断
2.方式1:单脉冲发生器   即可编程单脉冲发生器
3.方式2:速率波发生器
4.方式3:方波发生器
5.方式4:软件触发方式计数
6.方式5:硬件触发方式计数
8253控制字

初始化时,将控制字写入控制字端口0x43,
然后按照控制字设定顺序将频率写入指定的计数通道地址。
        movb $0x36, %al
        movl $0x43, %edx
        outb %al, %dx
        movl $11930, %eax        # timer frequency 100 HZ
        movl $0x40, %edx
        outb %al, %dx
        movb %ah, %al
        outb %al, %dx
即:
0x36即0x00110110
按照控制字解释即为选计数器1,先读低八位,再读高八位,
方式3方波,二进制,
然后将时间频率100(11930)分低八位,高八位写入计数器1对应端口0x40
代码含义为定时器通道0每隔10ms向中断控制器发送一次中断请求。(1s/100=0.01s=10ms)
本系列在本网站停止更新,更新请访问我的博客
http://blog.sina.com.cn/u/1025591593
上传的附件:
2011-1-9 15:16
0
雪    币: 56
活跃值: (37)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
66
看了头大,不知道有没新手教程!!
2011-1-9 16:26
0
雪    币: 81
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
67
这个你得多学学汇编,各种进制转换,取字节、取比特
这段就是写端口,照着说明写啊,没什么难得吧!
具体的8253的内部工作原理没必要知道。
入门看自己动手写操作系统,个人感觉不错。
2011-1-9 16:57
0
雪    币: 81
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
68
呵呵,忘了,这不是nasm语法。对不起!
$0x43表示常数0x43
%edx表示edx
movl $0x40, %edx
表示将0x40放入edx,跟nasm相反
outb也一样
2011-1-9 17:05
0
雪    币: 83
活跃值: (40)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
69
去搜索一下《自己动手写操作系统》pdf文档,下载下来用汉王pdf ocr或者acrobat reader professional之类的转换器转换为html,然后复制粘贴就ok了,有必要每天敲一点么?
2011-1-9 18:01
0
雪    币: 83
活跃值: (40)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
70
跟nasm无关,主要因为这是linux汇编,又叫AT&T 汇编,linux下的程序用gdb反汇编出来都是这样的,mov是反过来的,寄存器前面都有%
2011-1-9 18:06
0
雪    币: 81
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
71
你帮我找找这系列哪点跟那本书一样,谢谢!你看过吗?
2011-1-9 18:24
0
雪    币: 83
活跃值: (40)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
72
那你说你自己写的书了?
如果是买的书挺多用扫描仪扫描一下就成了电子书了
2011-1-9 19:12
0
雪    币: 81
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
73
呵呵,没见过不懂还像你这样狂妄的,看雪隐藏了这么多高手,相信你不是!
2011-1-9 19:18
0
雪    币: 563
活跃值: (95)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
74
越看越像单片机定时器
2011-1-9 19:19
0
雪    币: 81
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
75
这是linus写的代码节选
2011-1-9 19:20
0
游客
登录 | 注册 方可回帖
返回
//