标题:操作系统实验七:保护模式之中断测试实验
只看楼主
miaowangjian
Rank: 2
等 级:论坛游民
帖 子:34
专家分:30
注 册:2010-1-29
结帖率:100%
 问题点数:0 回复次数:3 
操作系统实验七:保护模式之中断测试实验
我对中断的理解:

    中断,顾名思义,就是在遇到特殊的情况时,停下当前正在做的事情,转去干其他(根据特殊情况预先设计好的)事情,完成后(并不一定非要)再继续之前做的事情。

   首先说说“去干其他(根据特殊情况预先设计好的)事情”。

   我们可以预先设计好256件事情(处理函数),然后使用0~255这256个编号(中断向量号)表示代指。“事情”根据是否可以再次打断(中断),分为中断门(不可以被打断)和陷阱门(可以再次被打断)。


   下面解释一下所谓的“遇到特殊的情况”,这个特殊的情况按照触发类型可以分为两类:

   自动产生:自动产生的意思就是程序自己根据需要自己主动触发的中断,也就是使用int指令+中断号触发的。对于所有的256个中断,都可以使用int指令触发。   

   被动产生:被动产生,就是系统硬性规定的,在遇到某种指定的情况,就触发对应中断向量号的中断。
具体而言如下表:(摘自《自己动手写内核系列_skelix》天衣有缝,有稍微修改)
中断向量号   触发原因
0x00           除零错
0x01           调试异常
0x02           非可屏蔽中断 (NMI)
0x03           断点 (INT 3 指令)
0x04           溢出 (INTO 指令)
0x05           越界 (BOUND 指令)
0x06           无效的指令
0x07           无协处理器
0x08           双重错误
0x09           协处理器越界
0x0A           无效的 TSS
0x0B           段不存在
0x0C           栈溢出
0x0D           通用保护异常(内存引用或其他检查保护),Windows 9x 蓝屏就是它的杰作
0x0E           页错误
0x0F           Intel 保留
0x10           协处理器错误
0x11-0x19      Intel 保留
需要说明的有两点:
    一.0x11~0x19这12个中断向量号, Intel预定了,但是并没有实际使用上,但将来可能会用到。因此我们可以在编写操作系统的使用直接拿来用,但是这样会有一定风险,要是Intel的下一代产品里使用了这些中断向量号,我们想要兼容它,就不得不修改代码将这几个中断向量号让出来。
    二.上表中触发的原因,基本都是程序运行时发生错误产生的(0x02号中断除外),可以称之为内部中断。与之相对应的称作外部中断的东西,就是由于计算机硬件(键盘,鼠标等)触发的。这些中断是可以屏蔽掉(忽略掉)的。想要响应这些中断,也需要分配相应的中断向量号才行。具体的内容就是下面要讲到的可编程中断控制器8259A。


可编程中断控制器8259A:

8259A分自从两块,每一块都可以接收8个不同触发事件的信号,具体如下:(摘自《自己动手写操作系统》于源)
主 8259A
序号        触发源头
IRQ0        时钟
IRQ1        键盘
IRQ2        来自 从 8259A
IRQ3        串口2
IRQ4        串口1
IRQ5        LPT2
IRQ6        软盘
IRQ7        LPT1

从 8259A
IRQ8        实时时钟
IRQ9        重定向IRQ2
IRQ10       保留
IRQ11       保留
IRQ12       PS\2 鼠标
IRQ13       FPU 异常
IRQ14       AT 温盘
IRQ15       保留
    通过编程,可以指定这些中断事件对应哪些中断向量号、是否使用 从 8259A ,以及屏蔽哪些中断信号。具体的方法请参考pm32.c中的Init8259A()函数。

    了解了中断源(触发源),以及中断源需要的一一对应关系的中断向量号,最后就是对各个中断向量号指定处理的动作(处理函数)了。这个我们在实验任务门的使用已经很熟悉了,就是要创建一个IDT(中断描述符表),这个IDT数组的序号就对应着中断向量号。通过宏Gate中的选择子和偏移量来指定处理的动作(处理函数)。使用lidt系统指令来加载这个IDT。
    需要特别说明的是,IDT这个数组中,每一个元素(中断)占8字节,若定义256个中断的话,需要2048字节的空间,这远远超出了引导扇区512字节的限制,因此与对付上一次实验中的TSS类似,直接使用内存中不会用到的空间。先是定义一个只有一个元素的IDT,然后通过汇编指令重复复制到预先决定好的内存地址,然后再初始化具体的内容(对特定的中断向量指定特定的处理函数)。


此次实验的流程:
    1.跳转到保护模式
    (pm32.c中main函数)
    2.重新加载新的GDT
    3.显示字符串:This is Protect model.
    4.为IDT模板设置一个默认函数
    5.将IDT模板重复复制到指定的内存地址
    6.为IDT 0x20中断(定时器中断)设置一个处理函数,不断循环加一修改显示的字符
    7.为IDT 0x80中断设置一个处理函数:显示字符“I”
    8.加载中断描述符IDT
    9.进行中断测试(使用int指令调用任意的中断,使用sti指令启用外部中断使得定时器中断产生效果等)

以下为此次实验代码:
code:run.c
程序代码:
//文件:run.c
//功能:编译操作系统的实验代码并创建img,生成bochs配置文件,运行bochs。
//说明:实验代码由16位部分引导程序与32位部分引导程序组成。
//      16位部分引导程序放在引导扇区的前半部分,0~79字节
//      32位部分引导程序放在引导扇区的后半部分,80~509字节
//      510、511字节放引导程序结束标记:0x55、0xaa
//运行:请使用yc09编译器编译运行,点击回车再次编译运行
//作者:miao
//时间:2010-1-13

#define FDISK_SIZE 1474560              //镜像大小:1.4MB

//虚拟机设置
char *pmSrc =
"megs: 32                                          \n"
"romimage: file=BIOS-bochs-latest, address=0xf0000 \n"
"vgaromimage: VGABIOS-elpin-2.40                   \n"
"floppya: 1_44=pm.img, status=inserted             \n"
"boot: a                                           \n"
"log: pm.out                                       \n"
"mouse: enabled=0                                  \n"
"keyboard_mapping: enabled=1, map=x11-pc-us.map    \n";

//因为yc09编译器不支持长返回指令 retf,所以使用这个函数作为临时解决方法。
//在汇编代码中需要用到 retf指令 时,使用 ret nop nop 这个三个连续指令代替,
//在编译代码后,调用此函数将ret指令的机器指令改为retf的机器指令。
//imgBuffer:编译后的二进制文件  size:文件的字节大小
//汇编与机器指令对照:ret == 0xc3   retf == 0xcb  nop == 0x90
void ret_To_retf(byte *imgBuffer,int size)
{
  while(--size)
    if(imgBuffer[size] == 0xc3)
      //这里保留着一个bug,若imgBuffer的最后两字节为0x3,会出现数组访问越界错误。
      if(imgBuffer[size+1] == 0x90 && imgBuffer[size+2] == 0x90)
      {
        imgBuffer[size] = 0xcb;
        printf("将一个ret改为了retf.\n");
      }
}

//编译指定代码文件并放入镜像指定位置
//filename:要编译的文件名 imgBuffer:保存到的镜像缓冲区 
//startIndex:指定起始位置 limitSize:编译后程序限定大小
int CompileFile(char *fileName, byte *imgBuffer, int startIndex, int limitSize,bool isneed)
{
  char *tempBuffer;   //保存部分引导程序的临时缓冲区

  //编译此部分引导程序,结果放到tempBuffer中
  int length = YC_CompileCpp(&tempBuffer, fileName, 0, 0);
  if(length <= 0 || length >= limitSize)
  {
    printf("文件: %s  中存在一些错误或文件过大(超过%d字节):%d字节\n", fileName,limitSize,length);
    return 1;
  }
  printf("文件: %s  编译成功,大小为:%d字节。\n", fileName, length);
 
  //将1此部分引导程序放到镜像引导扇区缓冲区指定起始位置
  memcpy(imgBuffer + startIndex, tempBuffer, length);
  free(tempBuffer);
  
  if(isneed)
  {
    ret_To_retf(imgBuffer + startIndex,length);
  }
  
  return 0;
}

int main(int argc, char **argv)
{
  char * filePath = argv[0];             //当前文件夹路径
  char fileName[MAX_PATH];               //用于缓存各个文件名
  //将可执行文件的完整路径去掉文件名,保留文件夹路径
  for( int i = strlen(filePath);filePath[i] != '\\';i--)
    filePath[i] = '\0';
  byte *imgBuffer = new byte[FDISK_SIZE];//镜像缓冲区

_start:
  //编译16位部分引导程序并放在引导扇区的前半部分,0~79字节
  if(CompileFile("pm16.c", imgBuffer, 0, 80,false))
    goto _restart;
  //编译32位部分引导程序并放在引导扇区的后半部分,80~509字节
  if(CompileFile("pm32.c", imgBuffer, 80, 512-80-2,true))
    goto _restart;

  //0000H-01FFH 为FAT引导扇区[第0扇区]  以55 AA标志结束 长度为200H(512)字节 
  imgBuffer[510] = 0x55;
  imgBuffer[511] = 0xaa;//标记软盘引导结尾

  //创建操作系统镜像pm.img
  if(YC_writefile("pm.img", imgBuffer, FDISK_SIZE) != FDISK_SIZE)
  {
    printf("写: %s 文件过程中出现错误。\r\n", fileName);
    goto _restart;
  }

  printf("\n%s 创建成功。\n",  fileName);

  //生成操作系统虚拟机配置文件pm.src
  YC_writefile("pm.src", pmSrc, strlen(pmSrc));
  //运行虚拟机
  YC_WinExec(strcat(strcpy(fileName, filePath), "bochs.exe"), "-q -f pm.src");

_restart:
  printf("\n点击回车重新编译运行!\n\n\n");
  while(getchar() != '\n');
  goto _start;
  return 0;
}

code:pm.h
程序代码:
//文件:pm.h
//功能:pm16.c与pm32.c的公共头文件
//运行:run.exe自动会编译pm16.c与pm32.c然后生成img并调用Bochs运行此程序
//提示:请先用yc09编译run.c文件,生成run.exe程序
//      之后修改pm16.c与pm32.c中代码,可直接运行run.exe查看效果,点击回车再次编译运行
//作者:miao
//时间:2010-2-12

//定义GDT属性
#define  DA_32         0x4000 //32位段
#define  DA_DRW        0x92   //存在的可读写数据段属性值
#define  DA_DRWA       0x93   //存在的已访问可读写数据段类型值
#define  DA_CR         0x9A   //存在的可执行可读代码段属性值
#define  DA_C          0x98   //存在的只执行代码段属性值

//定义门属性
#define  DA_386CGate   0x8c   //386调用门类型
#define  DA_386IGate   0x8e   //368中断们类型
#define  DA_386TSS     0x89   //可用386任务状态段类型值

typedef    unsigned int   t_32;  //4字节
typedef    unsigned short t_16;  //2字节
typedef    unsigned char  t_8;   //1字节
typedef    int            t_bool;//4字节
typedef unsigned int   t_port;//4字节

//存储段描述符/系统段描述符
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字节
};
#define Descriptor(bas,len,attr) { \
               (len) & 0xffff, \
               (bas) & 0xffff, \
               ((bas)>>16)&0xff, \
               (attr) & 0xff, \
               (((attr)>>8) &0xf0) + (((len)>>16) & 0x0f), \
               ((bas) >> 24) & 0xff } \

#define Gate(slector,offset,dCount,attr) { \
               (offset) & 0xffff, \
               slector, \
               (dCount)&0x1f , \
               attr, \
               ((offset)>>16) &0xff, \
               ((offset) >> 24) & 0xff } \

code:pm16.c
程序代码:
//文件:pm16.c
//功能:切换到保护模式,跳转到32位代码段
//说明:我试图仅在引导扇区编写保护模式的相关实验,因此将这个程序精简了很多。
//      它只负责跳转到保护模式,其他的工作都在pm32.c下完成。
//      pm16.c只占引导扇区的前半部分0~79字节。
//      pm32.c部分会加载到内存0x7c50处。
//运行:run.exe自动会编译pm16.c与pm32.c然后生成img并调用Bochs运行此程序
//提示:请先用yc09编译run.c文件,生成run.exe程序  
//      之后修改pm16.c与pm32.c中代码,可直接运行run.exe查看效果,点击回车再次编译运行
//作者:miao
//时间:2010-2-12

#define YCBIT 16     //告诉编译器,以16位格式编译程序
#define YCORG 0x7c00 //告诉编译器,在7c00处加载程序
#include "pm.h"

//GDT界限,只负责跳转到保护模式,到时会加载新的GDT
DESCRIPTOR label_gdt[] = 
{
  //         段基址   段界限   属性
  Descriptor(0,       0,       0),
  Descriptor(0x7c50,  0xfffff, DA_CR | DA_32),  //32位代码段(pm32.c),可执行可读
};
//GDT 选择子,根据GDT界限设置偏移量值
#define SelectorCode32 8*1 //指向32位段处

#pragma pack(1)
struct GDT_PTR
{
  unsigned short size;
  void *addr;
};
#pragma pack()
GDT_PTR  GdtPtr = {sizeof(label_gdt), (char*)label_gdt}; //段界限,基地址

asm void main()
{
  mov   ax, cs
  mov   ds, ax
  mov   es, ax

  //清屏
  mov   ah, 06h       //屏幕初始化或上卷
  mov   aL, 00h       //AH = 6,  AL = 0h
  mov   bx, 1110h     //蓝色底色
  mov   cx, 0         //左上角: (0, 0)
  mov   dl, 4fh       //第0列
  mov   dh, 1fh       //第0行
  int   10h           //显示中断
  
  lgdt  GdtPtr        //加载 GDTR

  //cli                 //关中断!!!!!!!注意这里被注销了!!!!!!!!!

  //打开地址线A20
  in    al, 92h
  or    al, 00000010b
  out   92h, al

  //准备切换到保护模式,置cr0的PE位为1
  mov   eax, cr0
  or    eax, 1
  mov   cr0, eax

  //真正进入保护模式
  jmp   dword SelectorCode32:0x0
}

code:pm32.c
程序代码:
//文件:pm32.c
//功能:保护模式下32位代码段,功能为加载新的GDT,设置默认中断处理函数,生成IDT描述符,
//      设置定时器中断处理函数等,进行中断调用测试
//说明:32位部分引导程序放在镜像引导扇区的后半部分,80~509字节中,程序大小不能超过这个限制
//运行:run.exe自动会编译pm16.c与pm32.c然后生成img并调用Bochs运行此程序
//提示:请先用yc09编译run.c文件,生成run.exe程序  
//      之后修改pm16.c与pm32.c中代码,可直接运行run.exe查看效果 ,点击回车再次编译运行
//作者:miao
//时间:2010-2-12

#define YCBIT 32  //告诉编译器,以32位格式编译程序
#define YCORG 0x0 //此值会对在编译时对变量函数等产生地址基址偏移量,简单起便,设置为0
#include "pm.h"

#define retf db 0xcb      //因为yc09不识别retf指令,所以使用宏定义一个retf指令
#define ProtecAddr 0x7c50 //进入保护模式后的程序基址

asm void Init8259A(); //初始化可编程中断控制器8259A
asm void SpuriousHandler(); //默认的中断处理函数
asm void ClockHandler(); //定时器中断处理函数,加一修改屏幕的一个字符
asm void UserIntHandler(); //软中断 0x80号中断处理函数 ,显示字符“I”
asm void DispStr();   //显示一个字符串,需要先设置好esi指向字符串地址,edi指向字符串的起始位置

//GDT 选择子,根据pm32.c中的GDT界限设置偏移量值
#define SelectorCode32       8*1 //指向32位段处代码段,可执行可读
#define SelectorVideo        8*2 //指向显存首地址
#define SelectorData32       8*3 //指向32位段处,这样,在程序中的变量就可以读写了

//GDT界限,注意,这个与pm16.c中的GDT不同,从pm16.c跳转过来后会立即载入这个新的GDT
DESCRIPTOR label_gdt[] = 
{
  //         段基址           段界限 属性
  Descriptor(0,                   0, 0),
  Descriptor(ProtecAddr,    0xfffff, DA_CR | DA_32 ),  //32位代码段(pm32.c),可执行可读,
                                                       //注意:必须要加 DA_32,否则 Bochs中调用iretd指令会出错:
                                                       //[CPU  ] iret: return CS selector null
  Descriptor(0xb8000,        0xffff, DA_DRW ),         //显存地址段,可读可写
  Descriptor(ProtecAddr,    0xfffff, DA_DRW | DA_32 ), //令32位代码段(pm32.c)的变量可以读写
};

#pragma pack(1)
struct GDT_PTR
{
  t_16 size;
  void *addr;
};
#pragma pack()
GDT_PTR GdtPtr = {sizeof(label_gdt), (char*)&label_gdt + ProtecAddr}; //段界限,基地址

#define IdtAddr  0x8000  //存放IDT描述符结构体的基址
#define IdtNum   0x81    //创建0x81=129个中断 (中断号:0x0~0x80)
//加载IDT时需要用到
GDT_PTR IdtPtr = {(IdtNum)*8, IdtAddr}; //段界限,基地址

//IDT中断描述符模板(为了节省程序字节空间,在此只是创建一个IDT模板,然后根据中断数重复拷贝到起始地址IdtAddr后)
DESCRIPTOR label_idt[] = 
{
  //   选择子          偏移量  参数个数  属性
  Gate(SelectorCode32, 0,      0,        DA_386IGate),
};

char Msg1[] = "This is Protect model.";

//32 位代码段. 由实模式跳入
asm void main()
{  
  lgdt  cs:GdtPtr            //加载新的GDTR
  
  mov   eax, SelectorVideo
  mov   gs, ax              //视频段选择子(目的)
  mov   eax, SelectorData32 //令32位代码段的变量(printPlace)可以读写
  mov   ds, ax
  
  //下面显示一个字符串(显示已经到达保护模式信息)
  mov   esi, &Msg1  //源数据偏移
  mov   edi, ((80 * 0 + 0) * 2) //目的数据偏移。屏幕第0行, 第0列。
  call  DispStr
  
  //为IDT模板设置一个默认函数
  mov   eax, &SpuriousHandler         //默认函数地址
  mov   word label_idt, ax            //放到偏移量前两个字节
  shr   eax, 16
  mov   word label_idt+6, ax          //放到偏移量后两个字节
  
  //在指定内存地址生成IDT描述符结构体数组
  mov   eax, IdtAddr     //目的,加上es的0x0
_CreateIDT:
  mov   esi, &label_idt //源,加上ds的0x7c50
  mov   edi, eax
  mov   ecx, 0x8 //有8字节,重复8次
  cld
  rep   movsb    //movs byte es:edi, ds:esi 此时:es=0x0,ds=0x7c50
  add   eax, 0x8
  cmp   eax,IdtAddr+IdtNum*0x8  //创建129个中断 (中断号:0x0~0x80)
  jne   _CreateIDT

  //为IDT 0x20中断(定时器中断)设置一个处理函数,不断循环加一修改显示的字符
  mov   eax, &ClockHandler         //默认函数地址
  mov   word es:[IdtAddr+0x20*0x8], ax            //放到偏移量前两个字节
  shr   eax, 16
  mov   word es:[IdtAddr+0x20*0x8+6], ax          //放到偏移量后两个字节
  
  //为IDT 0x80中断设置一个处理函数:显示字符“I”
  mov   eax, &UserIntHandler         //默认函数地址
  mov   word es:[IdtAddr+0x80*0x8], ax            //放到偏移量前两个字节
  shr   eax, 16
  mov   word es:[IdtAddr+0x80*0x8+6], ax          //放到偏移量后两个字节
  
  //加载中断描述符IDT
  lidt  IdtPtr
  call  Init8259A //初始化可编程中断控制器8259A
  
  //int   0x0  //测试其他默认中断
  int   0x80
  sti   //打开中断
  
_dead:
  jmp   _dead
}

//延时程序,给8259控制器一个反应时间
asm void IoDelay()
{
  nop
  nop
  nop
  nop
  ret
}
//初始化可编程中断控制器8259A
asm void Init8259A()
{
  mov   al, 0x11//0001001b:表示需要ICW4
  out   0x20, al//主8259,ICW1
  call  IoDelay
  
  out   0xa0, al//从8259,ICW1
  call  IoDelay
  
  mov   al, 0x20//IRQ0对应中断向量0x20
  out   0x21,al//主8259,ICW2
  call  IoDelay
  
  mov   al, 0x28//IRQ8对应中断向量0x28
  out   0xa1, al//从8259,ICW3
  call  IoDelay
  
  mov   al, 0x04//IR2对应从8259
  out   0x21, al//主8259,ICW3
  call  IoDelay
  
  mov   al, 0x02//对应I主8259的IR2
  out   0xa1, al//主8259,ICW3
  call  IoDelay
  
  mov   al, 0x01
  out   0x21,al//主8259,ICW4
  call  IoDelay
  
  out   0xa1, al//从8259,ICw4
  call  IoDelay

  mov   al, 0xfe//1111 1110 只开启定时器中断
  out   0x21, al//主8259,OCW1
  call  IoDelay
  
  mov   al, 0xff//1111 1111 屏蔽从8259所有中断
  out   0xa1, al//从8259,OCW1
  call  IoDelay
  
  ret
}

//默认的中断处理函数
asm void SpuriousHandler()
{
  mov   ah, 14h     //蓝底红字(ah = 14h)
  mov   al,'!'
  mov   gs:[((80 * 1 + 0) * 2)], ax

_dead:
  jmp   _dead
  iretd
}
//定时器中断处理函数,加一修改屏幕的一个字符
asm void ClockHandler()
{
  inc  word gs:[((80 * 2+ 0) * 2)]//覆盖的内容可以为空,连背景颜色一起修改
  inc  byte gs:[((80 * 3+ 0) * 2)]  //覆盖的内容必须不能为空,否则看不出效果
  mov   al, 0x20
  out   0x20, al//发送EOI

  iretd
}
//软中断 0x80号中断处理函数 ,显示字符“I”
asm void UserIntHandler()
{
  mov   ah, 14h     //蓝底红字(ah = 14h)
  mov   al,'I'
  mov   gs:[((80 * 3 + 0) * 2)], ax

  iretd
}

//显示一个字符串,需要先设置好esi指向字符串地址,edi指向字符串的起始位置
asm void DispStr()
{
  mov   ah, 14h     //蓝底红字(ah = 14h)
//循环逐个将字符串输出
_DispStr:
  mov   al, ds:[esi]//因为可读,才能用cs指向当前段的Msg字符串
  inc   esi
  cmp   al, '\0'    //判断是否字符串结束
  jz    _stop
  mov   gs:[edi], ax
  add   edi, 2
  jmp   _DispStr
_stop:             //显示完毕
  ret
}


[ 本帖最后由 miaowangjian 于 2010-2-13 08:16 编辑 ]
搜索更多相关主题的帖子: 操作系统 模式 实验 
2010-02-13 07:25
miaowangjian
Rank: 2
等 级:论坛游民
帖 子:34
专家分:30
注 册:2010-1-29
得分:0 
关于“操作系统实验”中的保护模式实验系列,到此将要暂时告一段落。
但对于保护模式实验来说,目前缺少一个最为重要的实验——保护模式的分页机制实验!
没有弄这个实验的主要原因是:我所设想的操作系统似乎用不到分页机制,而我又急于想要正式开始设计我的操作系统……

接下来应该有一次告别引导扇区512字节的引导程序实验,直接将内核放置到内存指定地址中。
关于我要弄的这个操作系统,将会在有了阶段性进展后才将代码贴出来(按照我现在的水平可能是遥遥无期……)。
不过我会尽可能的在编写时提取出实验测试代码作为单独的实验贴出来。

若有空闲的话,我也尽可能将保护模式的分页机制实验也写一下补上来。

对于如何设计操作系统内核,未来操作系统的发展趋势,会具有什么样的特点,希望各位高手能给我一些指导。恩,特别是如何支持中文的显示与输入的问题。

我对要弄的操作系统有一些简单的设想(妄想?):
1.类似于单核结构
2.将程序的运行的逻辑控制部分与执行部分完全分开(可能也就意味着不支持现有的高级语言,不直接支持现有的文件系统等等)
3.操作系统与数据库融为一体
4.弱化硬盘的概念
5.追求最大限度的自由
……
2010-02-13 09:15
chengstone
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
等 级:版主
帖 子:562
专家分:226
注 册:2004-4-3
得分:0 
想法不错 支持你把你的想法一步步实现

qq:69558139
2010-02-24 08:22
hu9jj
Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20
来 自:红土地
等 级:贵宾
威 望:396
帖 子:11713
专家分:43267
注 册:2006-5-13
得分:0 
默默地祝贺,如果需要测试者,也许我还能帮上一点忙。

活到老,学到老! http://www. E-mail:hu-jj@
2010-04-10 16:56



参与讨论请移步原网站贴子:https://bbs.bccn.net/thread-297633-1-1.html




关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.102513 second(s), 7 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved