• 保护模式下pmtest1.asm的理解


    整个代码对应内存线性地址分为四段,[gdt] [code32] [video32] [code16]

    代码先在实模式[code16]下运行,code16中的cs就是系统分配的该程序物理地址的基址.

    编译器会自动把其他段中的标号,编译成相对这个物理地址基址的偏移量.

    其他段的物理真实地址就是这个基址+标号所表示的偏移量.

    label_begin  label_code32这些是标签,会编译成相对该程序物理地址基址的偏移量.

    程序在内存中以二进制存在,cs指向程序在内存中开始的地方.

    Gdtptr 是一个数据结构,是GDTR数据结构.以便加载到GDTR中

    ldtr中是选择子,跟GDTR一样,系统会根据这个选择子,自动转换为LDT的基址.(系统转换的)  .所以某种程度上把ldtr看成和gdtr 一样存放的是ldt的基址.

     (以上是我个人的理解)

    ------------------------------------------------------------------------------------------------------------

    段机制轻松体验 
    内存寻址: 
    实模式下的内存寻址: 
    让我们首先来回顾实模式下的寻址方式 
    段首地址×16+偏移量 = 物理地址 
    为什么要×16?因为在8086CPU中,地址线是20位,但寄存器是16位的,最高寻址64KB,它无法寻址到1M内存。于是,Intel设计了这种寻址方式,先缩小4位成16位放入到段寄存器,用到时候,再将其扩大到20位,这也造成了段的首地址必须是16的倍数的限制。 
    公式:xxxx:yyyy 
    保护模式下分段机制的内存寻址: 
    分段机制是利用一个称作段选择符的偏移量,从而到描述符表找到需要的段描述符,而这个段描述符中就存放着真正的段的物理首地址,再加上偏移量 
    一段话,出现了三个新名词: 
    段选择子 
    描述符表 
    段描述符 
    ================================ 
    我们现在可以这样来理解这段话: 
    有一个结构体类型,它有三个成员变量: 
    段物理首地址 
    段界限 
    段属性 
    内存中,维护一个该结构体类型的数组。 
    而分段机制就是利用一个索引,找到该数组对应的结构体,从而得到段的物理首地址,然后加上偏移量,得到真正的物理地址。 
    公式:xxxx:yyyyyyyy 
    其中,xxxx也就是索引,yyyyyyyy是偏移量(因为32位寄存器,所以8个16进制)xxxx存放在段寄存器中。 
    ================================ 
    现在,我们来到过来分析一下那三个新名词: 
    段描述符:一个结构体,它有三个成员变量: 
    段物理首地址 
    段界限 
    段属性 
    描述符表:也就是一个数组,什么样的数组呢?是一个段描述符组成的数组。 
    段选择子:   也就是数组的索引,但这时候的索引不在是高级语言中数组的下标,而是我们将要找的那个段描述符相对于数组首地址(也就是全局描述表的首地址)偏移位置。 
    就这么简单,如图:

    图中,通过Selector(段选择子)找到存储在Descriptor Table(描述符表)中某个Descriptor(段描述符),该段描述符中存放有该段的物理首地址,所以就可以找到内存中真正的物理段首地址Segment 
    Offset(偏移量):就是相对该段的偏移量 
    物理首地址 + 偏移量 就得到了物理地址 本图就是DATA 
    但这时,心细的朋友就发现了一个GDTR这个家伙还没有提到! 
    我们来看一下什么是GDTR 
    Global Descriptor Table Register(全局描述符表寄存器) 
    但是这个寄存器有什么用呢 ? 
    大家想一下,段描述符表现在是存放在内存中,那CPU是如何知道它在哪里呢?所以,Iterl公司设计了一个全局描述符表寄存器,专门用来存放段描述符表的首地址,以便找到内存中段描述符表。 
    这时,段描述符表地址被存到GDTR寄存器中了。 
    ================================= 
    好了,分析就到这,我们来看一下正式的定义: 
    当x86 CPU 工作在保护模式时,可以使用全部32根地址线访问4GB的内存,因为80386的所有通用寄存器都是32位的,所以用任何一个通用寄存器来间接寻址,不比分段就可以访问4G空间中任意的内存地址。 
    但这并不意味着,此时段寄存器就不再有用了。实际上,段寄存器更加有用了,虽然再寻址上没有分段的限制了,但在保护模式下,一个地址空间是否可以被写入,可以被多少优先级的代码写入,是不是允许执行等等涉及保护的问题就出来了。要解决这些问题,必须对一个地址空间定义一些安全上的属性。段寄存器这时就派上了用场。但是设计属性和保护模式下段的参数,要表示的信息太多了,要用64位长的数据才能表示。我们把着64位的属性数据叫做段描述符,上面说过,它包含3个变量: 
    段物理首地址、段界限、段属性 
    80386的段寄存器是16位(注意:通用寄存器在保护模式下都是32位,但段寄存器没有被改变)的,无法放下保护模式下64位的段描述符。如何解决这个问题呢?方法是把所有段的段描述符顺序存放在内存中的指定位置,组成一个段描述符表(Descriptor Table);而段寄存器中的16位用来做索引信息,这时,段寄存器中的信息不再是段地址了,而是段选择子(Selector)。可以通过它在段描述符表中“选择”一个项目已得到段的全部信息。 
    那么段描述符表存放在哪里呢?80386引入了两个新的寄存器来管理段描述符,就是GDTR和LDTR,(LDTR大家先忘记它,随着学习的深入,我们会在以后学习)。 
    这样,用以下几步来总体体验下保护模式下寻址的机制 
    1、段寄存器中存放段选择子Selector 
    2、GDTR中存放着段描述符表的首地址 
    3、通过选择子根据GDTR中的首地址,就能找到对应的段描述符 
    4、段描述符中有段的物理首地址,就得到段在内存中的首地址 
    5、加上偏移量,就找到在这个段中存放的数据的真正物理地址。 
    好的,那我们开始编码,看看如何实现先前描述的内容 
    ================================= 
    首先,既然我们需要一个数组,全局描述符表,那我们就定义一块连续的结构体: 
    [SECTION .gdt] ;为了代码可读性,我们将这个数组放到一个节中 
    ;由一块连续的地址组成的,不就是一个数组吗?看下面代码,^_^ 
    段基地址 段界限 段属性 
    GDT_BEGIN: Descriptor 0,   0, 0 
    GDT_CODE32: Descriptor 0, 0, DA_C 
    ;上面,我定义了二个连续地址的结构体,大家先认为Descriptor就是一个结构体类型,我们会在以后详细讲述 
    ;第一个结构体,全部是0,是为了遵循Interl规范,先记得就OK 
    ;第二个定义了一个代码段,段基地址和段界限我们暂且还不知道,先初始化为0,但是因为是个代码段,代码段具备执行的属性,那么DA_C就代表是一个可执行代码段,DA_C是一个预先定义好的常量,我们会在详细讲解段描述符中讲解。 
    ================================= 
    我们继续来实现,那么下面,我们就需要设计段选择子了,因为上面代码已经包含了段描述符和全局描述符表 
    还记得选择子是个什么东西吗 ? 
    段选择子:   也就是数组的索引,但这时候的索引不在是高级语言中数组的下标,而是我们将要找的那个段描述符相对于数组首地址(也就是全局描述表的首地址)偏移位置。 
    看我代码怎么实现,包含以上代码不再说明: 
    [SECTION .gdt] 
    GDT_BEGIN: Descriptor 0, 0, 0 
    GDT_CODE32: Descriptor 0, 0, DA_C 
    ;下面是定义代码段选择子,它就是相对数组首地址的偏移量 
    SelectorCode32 equ GDT_CODE32 - GDT_BEGIN 
    ;因为第一个段描述符,不被使用,所以就不比设置段选择子了。 
    ================================= 
    偏移地址: 
    注意一点,我们在程序中使用的都是偏移地址,相对于段的偏移地址,用上面的例子来说,象 GDT_CODE32 GDT_BEGIN 这些结构体的首地址都是相对于数据段的偏移量。什么意思呢 ? 
    因为我们的程序到底加载到内存的哪个地方是不固定,不知道的,只需使用偏移地址操作就行了,如: 
    SelectorCode32 ,它本身就是一个偏移地址 
    但是SelectorCode32 equ GDT_CODE32 - GDT_BEGIN 
    怎么解释呢 ? 
    GDT_CODE32是相对于数据段的偏移量, 
    GDT_BEGIN也是相对于数据段的偏移量,虽然它是数组的首地址,说的罗索一些,GDT_BEGIN是数组的首地址(用数组的概念来理解页不错哦可以看作数组下标0),但是它是相对于数据段的偏移量 
    那么两个偏移量相减就是GDT_CODE32 相对于GDT_BEGIN的偏移量 (这个记住就行了,同时也是两个偏移量的长度)

    举个例子:0 1 2 3,一个偏移0表示占据 0这个地址,一个偏移3表示占据3这个地址(谈偏移要把前面要偏移的那个参照物拿掉,去掉要偏移的参照物(其实就可以数学表示成减前一个偏移参照物),剩下的就是偏移量),3这个地址相对与0这个地址的偏移量是把0这个地址先起掉后再算。(再结合数组来理解就可以了)
    所以,我们要时时刻刻记得,在程序中,我们永远使用的是偏移量,因为我们不知道程序将要被加载内存那块地方。 
    好了,基础也学的差不多了,下面我们要自己动手写一段程序,实现实模式到保护模式之间的跳转 
    =====================================================================
    ;实现从实模式到保护模式之间的跳转 
    ;参考:《自己动手写操作系统》 
    ---------------------------------------------------------------------- 
    %include "pm.inc"

    org 0100h 
    jmp LABEL_BEGIN 
    [SECTION .gdt] 
    GDT_BEGIN: Descriptor 0, 0,   0 
    GDT_CODE32: Descriptor 0, LenOfCode32 - 1, DA_C + DA_32   //程序段描述符的基地址首先置位0,以后还要重置为32位程序段物理首地址
    GDT_VIDEO: Descriptor 0B8000H, 0FFFFH,   DA_DRW                //这个32位程序段的物理首地址是在实模式下计算得到的。
    GdtLen equ $ - GDT_BEGIN       //长度=偏移量1---偏移量2 。如偏移4-偏移2得到长度为2 。  $表示当前的偏移量
    GdtPtr dw GdtLen - 1                  //定义了一个Gdtptr的数据结构,低16位dw部分为位段界限,高32位为0,一共48位,高32位以后还要重置

    dd 0                                             //0,1为低16位,高32位是从2开始,所以GdtPtr+2。高32位应该放GDT的物理地址
    ;定义段选择子 
    SelectorCode32 equ GDT_CODE32 - GDT_BEGIN 
    SelectorVideo equ GDT_VIDEO - GDT_BEGIN 
    [SECTION .main] 
    [BITS 16] 
    LABEL_BEGIN: 
    mov ax, cs 
    mov ds, ax                             //这个ds es ss等于cs 表示代码段和数据段在同一个街道上,只是偏移量不一样。
    mov es, ax 
    mov ss, ax                           //段寄存器就相当于街道号,偏移量就相当于门牌号。只有两者组合起来才能形成真正的物理地址。

                                              //看到段寄存器就应该想象成街道号,看到偏移量就应该想象成门牌号

                                              //如果代码中只出现偏移量,实际上也是和操作系统所默认的这个偏移量的段寄存器(只是代码没有显式给出而已)一起组成物理地址,(如ip它默认的段寄存器就是cs),代码也可以显式给出段寄存器和偏移量,这个时候的段寄存器就不一定是这个偏移量所默认的段寄存器。

    1、初始化32位代码段描述符的段基址
    ;我们可以在实模式下通过段寄存器×16 + 偏移两 得到物理地址, 
    ;那么,我们就可以将这个物理地址放到段描述符中,以供保护模式下使用, 
    ;因为保护模式下只能通过段选择子 + 偏移量 
    xor eax, eax 
    mov ax, cs 
    shl eax, 4 
    add eax, LABEL_CODE32 
    mov word [GDT_CODE32 + 2],ax     // 物理地址的ax放在段基址 2,3
    shr eax, 16 
    mov byte [GDT_CODE32 + 4],al      
    mov byte [GDT_CODE32 + 7],ah 
    2、得到段描述符表的物理地址,并将其放到GdtPtr中 
    xor eax, eax 
    mov ax, ds                                       // GDT的段地址为数据寄存器DS,
    shl eax, 4 
    add eax, GDT_BEGIN                     //DS加上偏移量GDT_BEGIN就是GDT的物理地址 
    mov dword [GdtPtr + 2],eax            //dword 表示是双字所以为32位,eax也是32位啊。

    ;加载到gdtr,因为现在段描述符表在内存中,我们必须要让CPU知道段描述符 表在哪个位置 
    ;通过使用lgdtr就可以将源加载到gdtr寄存器中 
    lgdt [GdtPtr] 
    3、关中断 
    cli 
    4、打开A20线 
    in al, 92h 
    or al, 00000010b 
    out 92h, al 
    5、准备切换到保护模式,设置PE为1 
    mov eax, cr0 
    or eax, 1 
    mov cr0, eax 
    ;现在已经处在保护模式分段机制下,所以寻址必须使用段选择子:偏移量来 寻址 

    6、跳转到32位代码段中 
    ;因为此时偏移量位32位,所以必须dword告诉编译器,不然,编译器将编译成16位 
    jmp dword SelectorCode32:0;跳转到32位代码段第一条指令开始执行

    [SECTION .code32] 
    [BITS 32] 
    LABEL_CODE32: 
    mov ax, SelectorVideo 
    mov es, ax 
    xor edi, edi 
    mov edi, (80 * 10 + 10) 
    mov ah, 0ch 
    mov al, 'G' 
    mov [es:edi],ax 
    jmp $ 
    LenOfCode32 equ $ - LABEL_CODE32 
    =================================== 
    这段代码的大概意思是: 
    先在16位代码段,实模式下运行,在实模式下,通过段寄存器×16+偏移量得到32位代码的真正物理首地址,并将放入到段描述符表中,以供在保护模式下使用,上面说过了,保护模式下寻址,是通过段选择子,段描述符表,段描述符一起工作寻址的。所以在实模式下所做的工作就是初始化段描述符表里的所有段描述符。 
    我们来看一下段描述符表,它有3个段描述符: 
    GDT_BEGIN 
    GDT_CODE32 
    GDT_VIDEO 
    GDT_BEGIN,遵循Intel公司规定,全部置0 
    GDT_CODE32,32位代码段描述符,供保护模式下使用 
    GDT_VIDEO,显存段首地址,我们知道,显存首地址是0B8000H. 
    回想一下,我们在实模式下往显示器上输出文字时,我们设置段寄存器为 
    0B800h,(注意后面比真正物理地址少一个0)。 
    而我们现在在保护模式下访问显存,那么0B8000h就可以直接放到段描述符中即可。因为段描述符中存放的是段的真正的物理地址。 
    下面我们来逐行分析该代码 
    org 0100h 
    这句话告诉加载器,将这段程序加载到偏移段首地址0100h处,即:偏移256字节处,为什么要加载到偏移256个字节处呢 ?

    这是因为,在DOS中,需要留下256个字节和DOS系统进行通信。 
    jmp LABEL_BEGIN 
    执行这句话就跳转到LABEL_BEGIN处开始执行。 
    好,我们看一下LABEL_BEGIN在那块,也就是16位代码段 
    [SECTION .main] 
    [BITS 16] 
    LABEL_BEGIN: (意味着运行在实模式)
    这样程序就从.main节的第一段代码开始执行。 
    我们看一下上面的代码,[BITS 16]告诉编译器,这是一个16位代码段,所使用的寄存器都是16位寄存器。 
    该代码段初始化所有段描述符表中的段物理首地址 
    首先在实模式下计算出32位代码段的物理首地址 
    对照 段值 × 16 + 偏移量 = 物理地址 
    1 mov ax, cs                                                        //
    2 shl eax, 4 ;向左移动4位,不就是×16吗?呵呵 //   CS存储着有操作体统分配的代码段值,段值*16 得到代码段的物理首地址(注意这是在实模  式下)
    ;到现在为止,eax就是代码段的物理首地址了,那么。。。看 
    3 add eax, LABEL_CODE32                              //  所有的诸如LABEL_CODE32:这样的都表示是偏移量,因为物理首地址的段值是由操作系统分配的。
    ;为eax (代码段首地址)加上 LABEL_CODE32偏移量,得到的不就是LABEL_CODE32的真正物理地址了吗 ?

    上面说过,代码中,使用的变量,或者标签 都是相对程序物理首地址的偏移量。如:LABEL_CODE32 这个标签就是相对程序物理首地址的偏移量。
    OK,现在我们已经知道了32位代码段的物理首地址,那么将eax放入到段描述符中就行了 
    我们先假设Descriptor就是一个结构体类型,(实际它是一个宏定义的数据结构,为了不影响整体思路,我们放到以后讲) 
    看一下这个Descriptor段描述符的内存模型: 
    ; 高地址………………………………………………………………………低地址 
    ; |   7   |   6   |   5   |   4   |   3   |   2   |   1   |   0   | 
    共 8 字节 
    ; |--------========--------========--------========--------========| 
    ; ┏━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┓ 
    ; ┃31..24┃   段属性   ┃   段基址(23..0)   ┃ 段界限(15..0)┃ 
    ; ┃   ┃       ┃   |       ┃       ┃ 
    ; ┃ 基址2┃       ┃基址1b│   基址1a   ┃   段界限1 ┃ 
    ; ┣━━━╋━━━┳━━━╋━━━━━━━━━━━╋━━━━━━━┫ 
    ; ┃   %6 ┃ %5 ┃ %4 ┃ %3 ┃   %2   ┃   %1   ┃ 
    ; ┗━━━┻━━━┻━━━┻━━━┻━━━━━━━┻━━━━━━━┛ 
    由于历史原因,段描述符的内存排列不是按照 段基地址 段界限 段属性 这样的来排列的,所以我们现在要想一种办法,把eax里所存放的物理首地址拆开,分别放到2,3,4,7字节处 
    那么很显然,我们可以将eax寄存器中的ax先放到2,3字节处 
    mov word [GDT_CODE32 + 2],ax  // 这种内存访问方式也是很常见的,首地址为数据寄存器提供(这里数据寄存器等于代码寄存器),GDT_CODE32为偏移量,再加2
    因为在偏移2个字节处,所以,首地址 + 2,才能定位到下标为2的字节开头处 
    而,word 告诉编译器,我要一次访问2个字节的内存 
    好,简单的搞定了,那么再看,我们现在要将eax高16字节分别放到下标为4,7字节处。 
    虽然eax的ax代表低16位,但是Intel并没有给高位一个名字定义,(不会是high ax,呵呵),所以,我们没有办法去访问高位。但是我们可以将高16位放到低16位中,因为这时,低16位我们已经不关心它的值了。 
    好,看代码 
    shr eax, 16 
    这句代码就将eax向右移动16位,低位被抛弃,高位变成了低位。呵呵。。。 
    现在好办了,低16位又可以分为al,和 ah,那么现在我们就将al放到4位置,ah放到7位置吧 
    mov byte [GDT_CODE32 + 4], AL 
    mov byte [GDT_CODE32 + 7], AH 
    不用我再解释这段代码了,自己去分析为什么吧。。。。

    //上面程序的功能是把32位程序段的物理首地址放到程序段描述符的段基址中,以便跳转到保护模式时,可以使用选择子选用程序段描述符,从而得到32位程序段的物理首地址。

    好了,32位代码段描述符设置好了,其界限设置看代码吧,为什么要那样设置,很简单的,界限 = 长度 - 1,段属性: 
    DA_C: 98h   可执行 
    DA_32: 4000h 32位代码段 
    是个常量,换算成二进制位,对照段描述符属性位置去看吧,参考任意一本保护模式书。 
    段描述符设置好了,但是,这段描述符表,还在内存中,我们必须想办法放到寄存器中,这时,就用到了gdtr(Golbal Descriptor Table Register),使用一条指令 
    lgdtr [GdtPtr] 
    就可以将GdtPtr加载到gdtr中 
    而gdtr的内存模型是: 
    高字节               低字节

    但GdtPtr是什么呢 ? 
    就是我们定义的和这个寄存器内存模型一摸一样的结构体: 
    GdtLen equ $ - LABEL_BEGIN 
    GdtPtr dw GdtLen - 1   ;界限 
    dd 0   ;真正物理地址 
    那现在我们就要计算GdtPtr第二个字节 也就是真正物理地址了 
    xor eax, eax 
    mov ax, ds 
    shl eax, 4 
    add eax, GDT_BEGIN 
    mov dword [GdtPtr + 2],eax              // dword表示为32位
    自己分析吧,和计算32位段首地址基本一样的, 
    搞定后,使用lgdt [GdtPtr]就将此加载到寄存器GDTR中了 
    然后关中断 
    cli 实模式下的中断和保护模式下的中断处理不一样,那就关吧,规矩 
    开启A20线 
    in al, 92h 
    or al, 00000010b 
    out 92h, al 
    如果不开启A20线,就无办法访问1M之上的内存,没办法,开启吧,规矩,想知道历史了,去查吧 
    然后设置CR0的PE位 
    mov eax, cr0 
    or eax, 1 
    mov cr0, eax 
    这个简单说一下,以后再详细 
    CR0也是一个寄存器,其中有个PE位,如果为0,就说明为实模式, 
    如果置1,说明为保护模式。现在我们要进入保护模式下工作,那么就要设置PE为1。 
    好了,看一下这个main节中的最后一个代码 
    jmp dword SelectorCode32 : 0 
    哈哈,现在已经再保护模式下了,当然要使用段选择子 + 偏移量来寻址啊,这样不就是寻址到了32位代码段中去了吗,偏移量为0不就说明从第一个代码开始执行。 
    不是吗 ?呵呵,那dword了? 
    因为现在的代码段是16位,编译器只能将它编译位16位,但处于保护模式下,它的偏移量应该是32位,所以,要显示告诉编译器,我这里使用的是32位,把我这块给编译成32位的!!! 
    如果不加dword, 
    jmp SelectorCode32:0 
    这句话不会出什么问题,16位的0是0,32位的0还是0,但如果这样呢?: 
    jmp SelectorCode32:0x12345678 
    跳转到偏移0x12345678中,这时就错了 
    如果不将dword,编译器就将该地址截断成16位,取低位,变成了0x5678 
    你说对吗 ?哈哈 
    所以我们必须这样做: 
    jmp dword SelectorCodde32:0x12345678 
    OKEY,我们继续追击,执行完上面那个跳转后, 
    代码就跳到了32位代码段的中,开始执行第一条指令 
    mov ax, SelectorVideo 
    再看 
    mov es,ax                         //现在已经在保护模式下,通过选择子找到显存的基址放

    呵呵,实模式下,放的是16位的段值,而现在呢,不就是要将段选择子放到段寄存器里吗 ?然后通过段选择子(偏移量)找到描述符表中对应的段描述符的吗 !!!! 
    继续看下面代码 
    xor edi, edi 
    mov edi, (80 * 10 + 10) 
    mov ah, 0ch 
    mov al, 'G' 
    跟实模式下差不多,设置目标10行10列 
    设置现实字符:G 
    mov [es:edi],ax 
    也和实模式下一样, 
    只不过实模式是这样来寻址 : 
    es×16 + edi 
    而保护模式下呢 
    es是一个偏移,根据这个偏移找到段描述符表中的对应显存段,然后这个显存段里存放的就是0B8000h,然后在加上偏移 不就的了吗!!! 
    哈哈 。。。。程序分析完毕,细节之处,自己体会去 
    总结: 
    1. 注意程序中使用的全部是偏移地址。注意两种偏移地址 
    A 对于程序的起始地址来说,所有变量和标签都是相对于整个程序的偏移量 
    B 对于段中定义的代码,有两种偏移: 
    相对于程序起始地址的偏移 
    相对于段标签的偏移。 
    2.不管是实模式下的物理地址,还是保护模式下的物理地址,反正他们都是物理地址,呵呵,实模式下求的物理地址,也能在保护模式下使用,只是他们不同的是,如何寻址的方式不一样。 
    3.一个程序中可以包含多个不同位的段,32位或者16位,他们之间也可以互相跳转,只是32位段用的是32位寄存器,16位代码段用的是16位寄存器,如果要在16位段下使用32位寄存器,必须象高级语言中强制类型转换一样,显示的定义 dword 
    参考: 《自动动手写操作系统》 
    《Undocument Windows 2000 Secrets》 
    《Linux 内核完全剖析》 
    发表于 @ 2008年09月14日 12:41:00 | 评论( 1 ) | 编辑| 举报| 收藏

    本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/wangshenwq/archive/2008/09/14/2927636.aspx

  • 相关阅读:
    HTML表单
    CSS等高布局的6种方式
    HTML用户反馈表单
    HTML美化修饰<A>
    sql查询语句 --------一个表中的字段对应另外一个表中的两个字段的查询语句
    jq 表格添加删除行
    js 静止f1到f12 和屏蔽鼠标右键
    手机自适应页面的meta标签
    tp3.2 的验证码的使用
    php多线程抓取网页
  • 原文地址:https://www.cnblogs.com/wanghj-dz/p/3976709.html
Copyright © 2020-2023  润新知