操作系统实验六:保护模式之使用调用门提升特权级+IA32分段机制中特权级简单介绍
关于IA32分段机制中特权级问题,我目前也还是十分迷糊,所以仅仅对目前涉及到特权级内容进行简单介绍,希望各高手对此实验中存在的错误或不当之处进行指正与补充说明。在IA32的分段机制里,分为4个特权等级(ring0~ring3):
Level0 高(内层)
L e v e l 1
L e v e l 2
L e v e l 3 低(外层)
L e v e l 1
L e v e l 2
L e v e l 3 低(外层)
各特权等级的区别在于对指令的限制(主要是系统指令的限制,如lgdt,lldt等)。这个非常容易理解,就是特权级越高,允许执行的指令越多。通过这个限制,普通应用程序无法使用一些特殊的系统指令(通常也不需要使用)。
若需要实现一些必须通过特权指令才能实现的功能时,可以通过操作系统提供的服务来实现。此时,就会涉及到特权等级的转变。假设普通程序运行在ring3,当它调用一个特权等级为0的系统服务时,在将执行权交给系统服务时,执行的特权提升为ring0(此时可以使用特权指令完成特定的功能),服务完成后,将执行权返还给普通程序时,执行权降为ring3(此时处于限制状态,只能执行普通指令)。
关于特权级的验证与转变,IA32有一套复杂的机制。为简单起见,在这里,仅对涉及到的地方做简单的介绍。
首先,需要对3个名词概念要有所认识:CPL、DPL、RPL。
当前特权级CPL(Current Privilege Level):
CPL表示的是当前程序运行时所处在的特权级别,存储在CS和SS寄存器中。一般而言,CPL与当前所处在的代码段的描述符中DPL相同(当前代码段为一致代码段时除外,因为目前尚未涉及到,在此不进行考虑)。
描述符特权级DPL(Descriptor Privilege Level):
DPL作为描述符(GDT、LDT等)属性中的一部分,存储在描述符的数据结构体中。当程序通过选择子使用或调用描述符中定义的数据段或代码段时,就会使用程序所具有的CPL值(一般而言其特权等级等于调用的代码段的在描述符中的DPL)与描述符中的DPL相比较,一般需要CPL的值小于等于被调用者的DPL,也就是说特权等级高的能调用特权级低的,随着代码段间不断相互调用,特权等级(CPL)只会不断降低或保持不变。若需要提高特权等级(执行特权级高的代码段),就需要用到调用门或其他手段了。
请求特权级RPL(Requested Privilege Level):
RPL作为选择子的属性之一存储在选择子这个数据结构的第一位和第二位。当程序通过选择子使用或调用描述符中的某个数据段或代码时,除了CPL会和DPL相比较以外,RPL也会与DPL相比较,若CPL的特权等级高于或等于DPL但RPL的特权等级却低于DPL时,也是无法成功调用的。
前面已经提到,若是直接调用的话,特权等级(CPL)只会越来越低,想要提高特权等级,可以通过任务门等手段。下面就看看使用任务门的一些情况。
首先看看调用门在描述符中的情况:最明显的,门描述符中包含一个选择子,由上一次实验可知,通过使用任务门,可以转到门任务中所包含的选择子所指向的代码段。一个程序调用任务门,必须遵循上面提到的规则,也就是不能向上越权调用任务门。而任务门也是不能包含任意的选择子来转到任意特权等级的代码段的,必须是任务门的DPL大于或等于所指向的代码段的DPL。也就是说,使用任务门,特权等级只会提高或保持不变。
我们使用jmp或call来调用调用门。对于jmp的使用也是有限制的,jmp只能调用那些DPL与所指向的代码段DPL相等的调用门。总结一下就是:可以暂时性的使用调用门提高特权等级执行一些代码然后返回低特权级代码段(使用call),若想要使用jmp使用调用门(可能一去不复返),那么必须保证使用调用门后特权等级依然不变。
下面回到此次实验中来。此次实验是体验在低特权级别下使用任务门提高特权等级。但是之前我们的实验一直是在默认的最高特权等级rong0运行的。因此需要现降低运行特权等级。
使用的方法就是:
假装自己是被一个低特权级别的代码段调用的(将ss,esp,cs,eip依次压入堆栈当前代码段所使用的堆栈),然后使用retf指令返回。
程序代码:
push 需要降到的代码段所使用的堆栈段的选择子(ss) push 需要降到的代码段所使用的堆栈段栈顶指针(esp) push 需要降到的代码段的选择子(cs) push 需要降到的代码段所执行到的指令条数(eip) retf
通过这个方法就可以降权跳转到一个特权级别更低的代码段。同时我们需要为两个不同特权等级的代码段提供两个不同堆栈段。但是由于引导扇区空间限制,我们不能再代码中显示声明一段空间来作为堆栈使用,而是直接通过宏指定不会用到的内存做为堆栈使用。
#define StackSpace 0x7c00 //堆栈地址 #define StackTop 0x4f //堆栈顶 #define StackSpace3 0x7a00 //堆栈地址 #define StackTop3 0x4f //堆栈顶
跳转到特权等级更高的代码段,我们必须要告诉系统这个在这个特权等级所使用的堆栈段是什么以及一些其他相关的信息,因此需要事先准备一个叫做TSS(Task-State Segment)的数据结构,TSS实际是保存各种寄存器到内存中以便以后使用的一组数据结构。同样因为引导扇区空间有限,直接接通过宏指定一段内存做为TSS(具体看pm.h中的代码)。
此次实验的流程:
1.跳转到保护模式
(pm32.c中main函数)
2.重新加载新的GDT
3.显示字符串:Protect.
4.为GDT中门任务函数初始化地址
5.为GDT中特权为3的函数初始化地址
6.初始化TSS并使用ltr指令加载
7.降权跳转到特权级为3的代码段(CodeRing3函数)
8.显示字符串:Ring 3.
9.使用call调用调用门跳转到CodeDest函数(默认特权级为0的代码段)
10.显示字符串:Gate.
11.不返回,而是在GDT的上设置好LDT的基地址并加载局部描述符(LDT)然后跳转到局部任务。
12.显示字符串:Local.
13.进入死循环。
以下是此次实验代码:
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-8 //定义GDT属性 #define DA_32 0x4000 //32位段 #define DA_DRW 0x92 //存在的可读写数据段属性值 #define DA_DRWA 0x93 //存在的已访问可读写数据段类型值 #define DA_CR 0x9A //存在的可执行可读代码段属性值 #define DA_C 0x98 //存在的只执行代码段属性值 //定义LDT属性 #define DA_LDT 0x82 //局部描述符表类型值 #define SA_TIL 0x4 //将TI位置1,表示是LDT选择子 //定义门属性 #define DA_386CGate 0x8c //386调用门类型 #define DA_386TSS 0x89 //可用386任务状态段类型值 //特权级别 #define DA_DPL0 0x00 //DPL = 0 #define DA_DPL3 0x60 //DPL = 3 //选择子类型 #define SA_RPL3 3 //RPL //为了节省空间,TTS的结构体不申请内存空间 //而是直接通过内存地址使用空闲的内存空间 #define TTS_Back 0x10000 #define TTS_TopOfStack 0x10004 // 0 级堆栈 #define TTS_SelectorStack 0x10008 #define TTS_CR1 0x1000c //1 级堆栈 #define TTS_SS1 0x10010 #define TTS_CR2 0x10014 //2 级堆栈 #define TTS_ss2 0x10018 #define TTS_CR3 0x1001c #define TTS_EIP 0x10020 #define TTS_EFLAGS 0x10024 #define TTS_EAX 0x10028 #define TTS_ECX 0x1002c #define TTS_EDX 0x10030 #define TTS_EBX 0x10034 #define TTS_ESP 0x10038 #define TTS_EBP 0x1003c #define TTS_ESI 0x10040 #define TTS_EDI 0x10044 #define TTS_ES 0x10048 #define TTS_CS 0x1004c #define TTS_SS 0x10050 #define TTS_DS 0x10054 #define TTS_FS 0x10058 #define TTS_GS 0x1005c #define TTS_LDT 0x10060 #define TTS_DebugTrapFlag 0x10064 //调试陷阱标志 #define TTS_IO 0x10066 //I/O位图基址 #define TTS_END 0x10068 //I/O位图结束标志 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-1-30 #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,加载TSS,降权调转到特权级为3的代码段CodeRing3函数, // 使用调用门提升进入特权级为0代码段CodeDest函数,初始化LDT,跳入LDT局部任务。 //说明: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-8 #define YCBIT 32 //告诉编译器,以32位格式编译程序 #define YCORG 0x0 //此值会对在编译时对变量函数等产生地址基址偏移量,简单起便,设置为0 #include "pm.h" #define retf db 0xcb //因为yc09不识别retf指令,所以使用宏定义一个retf指令 #define ProtecAddr 0x7c50 //进入保护模式后的程序基址 asm void CodeRing3(); //特权级为level 3的测试函数 asm void LDTCode(); //局部代码段 asm void CodeDest(); //门任务测试函数 asm void DispStr(); //显示一个字符串,需要先设置好esi指向字符串地址,edi指向字符串的起始位置 //LDT 选择子,根据pm32.c中的LDT界限设置偏移量值 #define SelectorLDTCodeA 8*0+SA_TIL //指向32位段局部任务处 //LDT界限 DESCRIPTOR label_ldt[] = { // 段基址 段界限 属性 Descriptor(ProtecAddr, 0xfffff, DA_CR | DA_32), //32位代码段(pm32.c),可执行可读 }; #define StackSpace 0x7c00 //堆栈地址 #define StackTop 0x4f //堆栈顶 #define StackSpace3 0x7a00 //堆栈地址 #define StackTop3 0x4f //堆栈顶 //GDT 选择子,根据pm32.c中的GDT界限设置偏移量值 #define SelectorCode32 8*1 //指向32位段处代码段,可执行可读 #define SelectorVideo 8*2 //指向显存首地址 #define SelectorData32 8*3 //指向32位段处,这样,在程序中的变量就可以读写了 #define SelectorStack 8*4 //指向Stack(堆栈),32位 #define SelectorLDT 8*5 //指向LDT,通过这个跳转到局部任务 #define SelectorCodeDest 8*6 //指向为门调用提供的测试任务 #define SelectorCodeRing3 (8*7+SA_RPL3) //指向特权级为3的测试函数 #define SelectorStack3 (8*8+SA_RPL3) //指向特权级为3的堆栈 #define SelectorTSS 8*9 //指向Stack(堆栈),从特权级3跳回特权级0要用的(记录在TSS中) //门选择子 #define SelectorCallGateTest (8*10+SA_RPL3) //指向门调用测试函数 //GDT界限,注意,这个与pm16.c中的GDT不同,从pm16.c跳转过来后会立即载入这个新的GDT DESCRIPTOR label_gdt[] = { // 段基址 段界限 属性 Descriptor(0, 0, 0), Descriptor(ProtecAddr, 0xfffff, DA_CR | DA_32), //32位代码段(pm32.c),可执行可读 Descriptor(0xb8000, 0xffff, DA_DRW | DA_DPL3), //显存地址段,可读可写 Descriptor(ProtecAddr, 0xfffff, DA_DRW | DA_32 | DA_DPL3), //令32位代码段(pm32.c)的变量可以读写 Descriptor(StackSpace, StackTop, DA_DRWA | DA_32), //Stack(堆栈),32位 Descriptor(0, 0xfffff, DA_LDT),//局部描述符,段基址和32位代码段相同,调用时需要加上偏移量 Descriptor(ProtecAddr, 0xfffff, DA_CR | DA_32),//为门调用提供的测试函数,可执行可读 Descriptor(0, 0xfffff, DA_CR | DA_32 | DA_DPL3), //CodeRing3 Descriptor(StackSpace3, StackTop3, DA_DRWA | DA_32 | DA_DPL3), //Stack3 Descriptor(TTS_Back, 0x68, DA_386TSS), //任务状态段TSS // 选择子 偏移量 参数个数 属性 Gate(SelectorCodeDest, 0, 0, DA_386CGate | DA_DPL3), }; #pragma pack(1) struct GDT_PTR { t_16 size; void *addr; }GdtPtr = {sizeof(label_gdt), (char*)&label_gdt + ProtecAddr}; //段界限,基地址 #pragma pack() char Msg1[] = "Protect."; char Msg2[] = "Ring 3."; char Msg3[] = "Gate."; char Msg4[] = "Local."; //32 位代码段. 由实模式跳入 asm void main() { lgdt cs:GdtPtr //加载新的GDTR mov eax, SelectorVideo mov gs, ax //视频段选择子(目的) mov eax, SelectorData32 //令32位代码段的变量(printPlace)可以读写 mov ds, ax mov ax, SelectorStack mov ss, ax mov esp,StackTop //下面显示一个字符串(显示已经到达保护模式信息) mov esi, &Msg1 //源数据偏移 mov edi, ((80 * 0 + 0) * 2) //目的数据偏移。屏幕第0行, 第0列。 call DispStr //为GDT中门任务函数初始化地址 xor eax, eax mov eax, &CodeDest + ProtecAddr mov word label_gdt+8*6+2, ax shr eax, 16 mov byte label_gdt+8*6+4, al mov byte label_gdt+8*6+7, ah //为GDT中特权为3的函数初始化地址 xor eax, eax mov eax, &CodeRing3 + ProtecAddr mov word label_gdt+8*7+2, ax shr eax, 16 mov byte label_gdt+8*7+4, al mov byte label_gdt+8*7+7, ah //初始化TSS mov dword [TTS_TopOfStack - ProtecAddr], StackTop //记录特权为0的堆栈 mov dword [TTS_SelectorStack - ProtecAddr], SelectorStack //记录特权为0时所使用的堆栈选择子 mov word [TTS_IO - ProtecAddr], TTS_END //I/O位图基址 mov byte [TTS_END - ProtecAddr], 0xff //I/O位图结束标志 //加载TSS mov ax, SelectorTSS ltr ax //为降到特权级为3做准备 push SelectorStack3 push StackTop3 push SelectorCodeRing3 push 0 retf //通过retf实现降权并调转到特权级为3的测试函数 _dead: jmp _dead } //特权级为level 3的测试函数 asm void CodeRing3() { mov ax, SelectorVideo //因为特权级为3,所以视频段也要为level 3 mov gs, ax //视频段选择子(目的) //下面显示一个字符串(显示已经到达保护模式信息) mov esi, &Msg2 //源数据偏移 mov edi, ((80 * 1 + 0) * 2) //目的数据偏移。屏幕第1行, 第0列。 call DispStr call SelectorCallGateTest:&CodeDest //通过门调用,回到特权级0 _dead: //不会执行到这里,只是为了检测是否越界而已 jmp _dead } //门测试函数 asm void CodeDest() { mov ax, SelectorVideo mov gs, ax //下面显示一个字符串(显示已经到达门任务信息) mov esi, &Msg3 //源数据偏移 mov edi, ((80 * 2 + 0) * 2) //目的数据偏移。屏幕第2行, 第0列。 call DispStr //在GDT的上设置好LDT的基地址,然后加载局部描述符(LDT) xor eax, eax mov eax, &label_ldt + ProtecAddr mov word label_gdt+SelectorLDT+2, ax shr eax, 16 mov byte label_gdt+SelectorLDT+4, al mov byte label_gdt+SelectorLDT+7, ah mov ax, SelectorLDT lldt ax jmp SelectorLDTCodeA:&LDTCode //跳转到局部任务 _dead: //不会执行到这里,只是为了检测是否越界而已 jmp _dead } //局部代码段 asm void LDTCode() { mov ax, SelectorVideo mov gs, ax //下面显示一个字符串(显示已经到达保护模式信息) mov esi, &Msg4 //源数据偏移 mov edi, ((80 * 3 + 0) * 2) //目的数据偏移。屏幕第3行, 第0列。 call DispStr _dead: //最后在这里停止 jmp _dead } //显示一个字符串,需要先设置好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 }