• GDB TUI


    调试代码的时候,只能看到下一行,每次使用list非常烦,不知道当前代码的context 

    http://beej.us/guide/bggdb/#compiling

    简单来说就是在以往的gdb开始的时候添加一个-tui选项.有的版本已经有gdbtui这个程序了

    在linux自带的终端里是正常显示的,但是在securecrt里面,可能由于编码的问题,边缘会有些乱码,不过不影响使用(如果你的程序有错误输出,会扰乱整个界面,所以在调试的时候,建议添加2>/dev/null,这样的话基本可用) 

    启动gdb之后,上面是src窗口,下面是cmd窗口,默认focus在src窗口的,这样的话上下键以及pagedown,pageup都是在移动显示代码,并不显示上下的调试命令.这个时候要切换focus,具体可简单参见

    (gdb) info win  查看当前focus
            SRC     (36 lines)  <has focus>
            CMD     (18 lines)
    (gdb) fs next  切换focus
    Focus set to CMD window.
    (gdb) info win 
            SRC     (36 lines)
            CMD     (18 lines)  <has focus>
    (gdb) fs SRC  切换指定focus
    Focus set to SRC window.
    (gdb)
    

    (Window names are case in-sensitive.)

      

    To start in neato and highly-recommended GUI mode, start the debugger with gdb -tui. (For many of the examples, below, I show the output of gdb's dumb terminal mode, but in real life I use TUI mode exclusively.)

    And here is a screenshot of what you'll see, approximately:

    In TUI mode, the layout command controls which windows you see. Additionally, the tui reg allows control of the register window, and will open it if it's not already open.

    The commands are:

    layout src Standard layout—source on top, command window on the bottom
    layout asm Just like the "src" layout, except it's an assembly window on top
    layout split Three windows: source on top, assembly in the middle, and command at the bottom
    layout reg Opens the register window on top of either source or assembly, whichever was opened last
    tui reg general Show the general registers
    tui reg float Show the floating point registers
    tui reg system Show the "system" registers
    tui reg next Show the next page of registers—this is important because there might be pages of registers that aren't in the "general", "float", or "system" sets

     

     

    1 处于TUI模式的GDB

    为了以TUI模式运行GDB,可以在调用GDB时在命令行上指定-tui选项,或者处于非TUI模式时在GDB中使用Ctrl+X+A组合键。如果当前处于TUI模式,后一种命令方式就会使你离开TUI模式。

    在TUI模式中,GDB窗口划分为两个子窗口——一个用于输入GDB命令,而另一个用于查看源代码。

    例如:

    源代码为ins.c

    复制代码
    #include <stdio.h>
    
    int x[10],
            y[10], 
            num_inputs,                                     
            num_y = 0;
    
    
    void get_args(int ac,char **av){
            int i;
            num_inputs = ac - 1;
            for(i = 0;i < num_inputs;++i)
                    x[i] = atoi(av[i + 1]);
    }
    
    void scoot_over(int jj){
            int k;
            for(k = num_y;k > jj;--k)
                    y[k] = y[k - 1];
    }
    
    void insert(int new_y){
            int j;
            //
            if(num_y==0){
                    y[0] = new_y;
                    return;
            }
    
            for(j = 0;j < num_y;++j){
                    if(new_y < y[j]){
                            scoot_over(j);
                            y[j] = new_y;
                            return;
                    }
            }
            y[num_y]=new_y;
    }
    
    void process_data(){
            for(num_y = 0;num_y < num_inputs;++num_y)
                    insert(x[num_y]);
    }
    
    void print_results(){
            int i;
            for(i = 0;i < num_inputs;++i)
                    printf("%d
    ",y[i]);
    }
    
    int main(int argc,char **argv){
            get_args(argc,argv);
            process_data();
            print_results();
    }
    复制代码

    编译后:

    gcc -g3 -Wall -o insert_sort ins.c

    注意,在GCC中可以用-g选项让编译器将符号表(即对应于程序的变量和代码行的内存地址列表)保存在生成的可执行文件(这里是insert_sort)中。这是一个绝对必要的步骤,这样才能在调试会话过程中引用源代码中的变量名和行号。

    使用GDB调试insert_sort

    如果正在使用GDB但没有使用TUI模式,则位于下方的子窗口确切地显示你将看到的内容。此处,该子窗口显示如下内容。

    1)发出一条break命令,在当前源文件第12行处设置断点。

    2)执行run命令运行程序,并且向该程序传递命令行参数12、5、6.在此之后,调试器在指定的断点处停止执行。GDB会提醒用户断点位于ins.c的第12行,并且通知该源代码行的机器代码驻留在内存地址0xbffff484中。

    3)发出next命令,步进到下一个代码行,即第13行。

    2 主要的调试操作

    退出GDB:quit或者Ctrl+d

    执行程序:run

    2.1 单步调试源代码

    安排程序的执行在某个地方暂停,以便检查变量的值,从而得到关于程序错误所在位置的线索。

    • 断点

    调试工具会在指定断点处暂停程序的执行。在GDB中是通过break命令及其行号完成的。

    普通断点和条件断点

    (gdb) break 30

    Breakpoint 1 at 0x80483fc: file  ins.c,line 30.

    (gdb) condition 1 num_y==1

    第一个命令在第30行放置一个断点。这里的第二个命令condition 1 num_y==1使得该断点称为有条件的断点:只有当满足条件num_y==1时,GDB才会暂停程序的执行。

    注意,与接受行号(或函数名)的break命令不同,condition接受断点号。总是可以用命令info  break来查询要查找的断点的编号。

    用break if可以将break和condition命令组合成一个步骤,如下所示:

    (gdb) break 30 if num_y==1

    • 单步调试

    前面提到过,在GDB中next命令会让GDB执行下一行,然后暂停。step命令的作用与此类型,只是函数调用时step命令会进入函数,而next导致程序执行的暂停出现在下次调用函数时。

    • 恢复操作

    在GDB中,continue命令通知调试器恢复执行并继续,直到遇到断点为止。

    • 临时断点

    在GDB中,tbreak命令与break相似,但是这一命令设置的断点的有效期限只到首次到达指定行时为止。

    2.2 检查变量

    (gdb) print j

    $1=1

    对GDB的这一查询的输出表明j的值为1.$1标签意味着这是你要求GDB输出的第一个值。($1、$2、$3等表示的值统称为调试会话的值历史。)

    2.3 在GDB中设置监视点以应对变量值的改变

    监视点结合了断点和变量检查的概念。最基本形式的监视点通知调试器,每当指定变量的值发生变化时都暂停程序的执行

    (gdb) watch z

    当运行程序时,每当z的值发生变化,GDB都会暂停执行。

    更好的方法是,可以基于条件表达式来设置监视点。例如,查找执行期间z 的值大于28的第一个位置

    (gdb) watch(z>28)

    2.4 上下移动调用栈

    在函数调用期间,与调用关联的运行时信息存储在称为栈帧的内存区域中。帧中包含函数的局部变量的值、其形参,以及调用该函数的记录。每次发生函数调用时,都会创建一个新帧,并将其推导一个系统维护的栈上;栈最上方的帧表示当前正在执行的函数,当函数退出时,这个帧被弹出栈,并且被释放。

    在GDB中可用用如下命令查看以前的帧:

    (gdb) frame 1

    当执行GDB的frame命令时,当前正在执行的函数的帧被编号为0,其父帧(即该函数的调用者的栈帧)被编号为1,父帧的父帧被编号为2,以此类推。GDB的up命令将你带到调用栈中的下一个父帧(例如,从帧0到帧1),down则引向相反方向。

    显示整个栈:backtrace

    浏览以前的GDB命令:上一个Ctrl+P、下一个Ctrl+N

    3 联机帮助

    在GDB中,可以通过help命令访问文档。例如:

    (gdb) help breakpoints

    4 启动文件的使用

    在重新编译代码时,最好不要退出GDB。这样,你的断点和建立的其他各种动作都会保留。要是退出GDB,就不得不再次重复键入这些内存。

    然而,在完成调试前可能需要退出GDB。如果你要离开一段时间,而且不能保持登录在计算机中,则需要退出GDB。为了不丢失它们,可以将断点和设置的其他命令放在一个GDB启动文件中,然后每次启动GDB时都会自动加载它们。

    GDB的启动文件默认名为.gdbinit。

    在调用GDB时可以指定启动文件。例如,

    $ gdb -command=z x

    表示要在可执行文件x上运行GDB,首先要从文件z中读取命令。

    5 gdb暂停机制

    有3种方式可以通知GDB暂停程序的执行。

    1)断点:通知GDB在程序中的特定位置暂停执行。

    2)监视点:通知GDB当特定内存位置的值发生变化时暂停执行

    3)捕获点:通知GDB当特定事件发生时暂停执行。

    GDB中使用delete命令删除断点:

    (gdb) help delete

    5.1 断点概述

    GDB中关于断点“位置”的含义非常灵活,它可以指各种源代码行、代码地址、源代码文件中的行号或者函数的入口等。

    例如:

    break 35

    这里指GDB执行到第34行,但是第35行还没有执行。断点显示的是将要执行的代码行。

    5.2 跟踪断点

    程序员创建的每个断点(包括断点、监视点和捕获点)都被标识为从1开始的唯一整数标识符。这个标识符用来执行该断点上的各种操作。

    5.2.1 GDB中的断点列表

    当创建断点时,GDB会告知你分配给该断点的编号。例如,

    (gdb) break main
    Breakpoint 1 at 0x8048569: file ins.c, line 52.

    被分配的编号是1.如果忘记了分配给哪个断点的编号是什么可以使用info  breakpoints命令来提示。

    (gdb) info breakpoints
    Num Type Disp Enb Address What
    1 breakpoint keep y 0x08048569 in main at ins.c:52
    2 breakpoint keep y 0x0804847e in insert at ins.c:25
    3 breakpoint keep y 0x08048491 in insert at ins.c:30

    如果想要删除断点,可以通过delete命令以及断点标识符,例如

    (gdb) delete 1 2 3

    5.3 设置断点

    5.3.1 在GDB中设置断点

    1)break function

    在函数function()的入口处设置断点。

    (gdb) break main

    在main函数的入口处设置断点

    2)break line_number

    在当前活动源代码文件的line_number处设置断点。

    (gdb) break 35

    在当前显示的源文件的35行设置了一个断点。

    3)break filename:line_number

    在源代码文件filename的line_number处设置断点。如果filename不在当前工作目录中,则可以给出相对路径名或者完全路径名来帮助GDB查找该文件,例如:

    (gdb) break source/bed.c:35

    4)break filename:function

    在文件filename中的函数function()的入口处设置断点。重载函数或者使用同名静态函数的程序可能需要使用这种形式,例如:

    (gdb) break bed.c:parseArguments

    正如我们看到的,当设置一个断点时,该断点的有效性会持续到删除、禁用或者退出GDB时。然而,临时断点时首次到达后就会被自动删除的断点。临时断点使用tbreak命令设置。

    C++允许重载函数,使用break function会在所有具有相同名称的函数上设置断点。如果要在函数的某个特定实例上设置断点,需要没有歧义,则使用源文件中的行号。

    GDB实际设置断点的位置可能和我们请求将断点放置的位置不同。
    比如下列代码:
    复制代码
    int main(void)  
    {  
        int i;  
        i = 3;  
      
       return 0;  
    }  
    复制代码

    如果我们尝试在函数main入口处设置断点,断点实际会被设置在第4行。因为GDB会认为第三行的机器码对我们的调试目的来说没有用处。

    5.4 多文件中的断点设置

    例如:

    main.c

    复制代码
    #include<stdio.h>
    void swap(int *a,int *b);
    
    int main()
    {
        int i=3;
        int j=5;
        printf("i:%d,j:%d
    ",i,j);
        swap(&i,&j);
        printf("i:%d,j:%d
    ",i,j);
    
        return 0;
    }
    复制代码

    swap.c

    void swap(int *a,int *b)
    {
        int c=*a;
        *a=*b;
        *b=c;
    }

    在main上设置断点:

    (gdb) break main
    Breakpoint 1 at 0x80483cd: file main.c, line 6.

    在swap上设置断点的方法:

    (gdb) break swapper.c:1
    Breakpoint 2 at 0x804843a: file swapper.c, line 1.
    (gdb) break swapper.c:swap
    Note: breakpoint 2 also set at pc 0x804843a.
    Breakpoint 3 at 0x804843a: file swapper.c, line 3.
    (gdb) break swap
    Note: breakpoints 2 and 3 also set at pc 0x804843a.
    Breakpoint 4 at 0x804843a: file swapper.c, line 3.

    每个GDB都有一个焦点,可以将它看作当前“活动”文件。这意味着除非对命令做了限定,否则都是在具有GDB的焦点的文件上执行命令。默认情况下,具有GDB的初始焦点的文件是包含main()函数的文件,但是当发生如下任一动作时,焦点会转移到不同的文件上。

    1)向不同的源文件应用list命令

    2)进入位于不同的源代码文件中的代码

    3)当在不同的源代码文件中执行代码时GDB遇到断点

    例如:

    (gdb) break 6
    Note: breakpoint 1 also set at pc 0x80483cd.
    Breakpoint 5 at 0x80483cd: file main.c, line 6.

    当前焦点是main.c,所以在main.c中设置。

    (gdb) list swap
    (gdb) break 6
    Breakpoint 6 at 0x8048454: file swapper.c, line 6.

    现在的焦点是swapper.c。

    (gdb) info breakpoints
    Num Type Disp Enb Address What
    1 breakpoint keep y 0x080483cd in main at main.c:6
    2 breakpoint keep y 0x0804843a in swap at swapper.c:1
    3 breakpoint keep y 0x0804843a in swap at swapper.c:3
    4 breakpoint keep y 0x0804843a in swap at swapper.c:3
    5 breakpoint keep y 0x080483cd in main at main.c:6
    6 breakpoint keep y 0x08048454 in swap at swapper.c:6

    5.5 断点的持久性

    如果在修改和重新编译代码时没有退出GDB,那么在下次执行GDB的run命令时,GDB会感知到代码已修改,并自动重新加载新版本。

    5.6 删除和禁用断点

    在调试会话期间,有时会发现有的断点不再使用。如果确认不再需要断点,可以删除它。也许你不想删除它,而是打算将它虚置起来,这称为禁用断点。如果以后再次需要,可以重新启用断点。

    2.6.1 在GDB中删除断点

    如果确认不再需要当前断点,那么可以删除该断点。

    delete命令用来基于标识符删除断点,clear命令使用和创建断点的语法删除相同。

    1)delete breakpointer_list

    删除断点使用数值标识符。断点可以是一个数字,比如delete 2 删除第2个断点;也可以是数字列表,不然delete 2 4 删除第二个和第四个断点。

    2)delete

    删除所有断点。

    3)clear

    清除GDB将执行的下一个指令处的断点。这种方法适用于要删除GDB已经到达的断点额情况。

    4)clear function、clear filename:function、clear line_number和clear filename:line_number

    2.6.2 在GDB中禁用断点

    每个断点都可以禁用和启用。只有遇到启用的断点时,才会暂停程序的执行;它会忽略禁用的断点。

    为什么要禁用断点呢?在调试会话期间,会遇到大量断点。对于经常重复的循环结构或函数,这种情况使得调试极不方便。如果要保留断点以便以后使用,暂时又不希望GDB停止执行,可以禁用它们,在以后需要时再启用。

    使用disable breakpoint-list命令禁用断点,使用enable breakpoint-list命令启用断点。

    例如,

    (gdb) disable 3

    将禁用第三个断点

    (gdb) enable 1 5

    将启用第一个和第五个断点。

    不带任何参数地执行disable命令将禁用所有现有断点。类似的,不带任何参数的enable命令将启用所有断点。

    还有一个enable once命令,在得到下次引起GDB暂停执行后被禁用。语法为:

    enable once breakpoint-list

    例如,enable once 3 会使得断点3 在下次导致GDB停止程序的执行后被禁用。这个命令与tbreak命令非常类似,但是当遇到断点时,它是禁用断点,而不是删除断点。

    2.6.3 浏览断点属性

    info breakpoints命令(简写 i b)来获得设置的所有断点的清单,以及它们的属性。

    例如:

    (gdb) info breakpoints
    Num Type Disp Enb Address What
    1 breakpoint keep y 0x080483cd in main at main.c:6
    2 breakpoint keep y 0x0804843a in swap at swapper.c:1
    3 breakpoint keep y 0x0804843a in swap at swapper.c:3
    4 breakpoint keep y 0x0804843a in swap at swapper.c:3
    5 breakpoint keep y 0x080483cd in main at main.c:6
    6 breakpoint keep y 0x08048454 in swap at swapper.c:6

    7 hw watchpoint keep y                         counter

    让我们分析info breakpoints的这一输出:

    1)标识符(num):断点的唯一标识符

    2)类型(type):这个字段指出该断点是断点、监视点还是捕获点

    3)部署(disp):每个断点都有一个部署,指示断点下次引起GDB暂停程序的执行后该断点上会发生什么事情。

    保持(keep),下次到达断点后不改变断点

    删除(del),下次到达断点后删除断点,临时断点(tbreak设置)

    禁用(dis),下次到达后会禁用断点,使用enable once命令设置的

    4)启用状态(enb):这个字段说明断点当前是启用还是禁用的

    5)地址(Address):这是内存中设置断点的位置。

    6)位置(what):what字段显示了断点所在的位置的行号和文件名

    6 恢复执行

    恢复执行的方法有3类。第一类是使用step和next“单步”调试程序,仅执行代码的下一行然后再次暂停。第二类由使用continue组成,使GDB无条件地恢复程序的执行,直到遇到另一个断点或程序结束。最后一类方法涉及条件:用finish或until命令恢复。在这种情况下,GDB会恢复执行;程序继续运行直到遇到某个预先确定的条件(比如,到达函数的末尾),到达另一个断点,或者程序完成。

    6.1 使用step和next单步调试

    一旦GDB在断点处停止,可以使用next(简写n)和step(简写s)命令来单步调试代码。

    这两个命令的不同之处在于它们如何处理函数调用:next执行函数,不会在其中暂停,然后在调用之后的第一条语句处暂停。而step在函数中的第一个语句处暂停。step命令会进入调用的函数,这称为单步进入函数。而next永远不会离开main()。这是两个命令的主要区别。next将函数调用看做一行代码,并在一个操作中执行整个函数,这称为单步越过函数。

    然而,似乎next越过调用的函数主体,但是它并未真的单步“越过”任何内容。GDB安静地执行调用函数的每一行,不向我们展示细节。

    6.2 使用continue恢复程序执行

    第二种恢复执行的方法是使用continue命令,简写为c。这个命令使GDB恢复程序的执行,直到触发断点或者程序结束。

    continue命令可以接受一个可选的整数参数n。这个数字要求GDB忽略下面n个断点。例如,continue 3让GDB恢复程序执行,并忽略接下来的3个断点。

    6.3 使用finish恢复程序执行

    一旦触发了断点,就使用next和step命令逐行执行程序。有时这是一个痛苦的过程。

    有时使用step进入的调用的函数,查看了几个变量的信息,如果没有兴趣单步调试其余部分,想返回到单步进入被调用函数之前GDB所在的调用函数。然而,如果要做的只是跳过函数的其余部分,那么再设置一个无关断点并使用continue似乎比较浪费。这是可以使用finish命令。

    finish命令(简写为fin)指示GDB恢复执行,直到恰好在当前栈帧完成之后为止。也就是说,这意味着如果你在一个不是main()的函数中,finish命令会导致GDB恢复执行,直到恰好在函数返回之后为止。

    虽然可以键入next 3 而不是finish,但是后者更容易。

    finish的另一个常见用途是当不小心单步进入原本希望单步越过的函数时(换言之,当需要使用next时使用了step)。在这种情况下,使用finish可以讲你正好放回到使用next会位于的位置。

    如果在一个递归函数中,finish只会将你带到递归的上一层。

    6.4 使用until恢复程序执行

    finish命令在不进一步在函数中暂停(除了中间断点)的情况想完成当前函数的执行。类似地,until命令(简写为u)通常用来在不进一步在循环中暂停(除了循环中的中间断点)的情况下完成正在执行的循环。

    当i很大是,使用next需要多次。而使用until会执行循环的其余部分,让GDB在循环后面的第一行代码处暂停。当然,如果GDB在离开循环前遇到一个断点,它就会在那里暂停。

    7 条件断点

    只要启用了断点,调试器就总是在该断点处停止。然而,有时有必要告诉调试器只有当符合某种添条件时才在断点处停止。

    7.1 设置条件断点

    break break-args if (condition)

    其中brea-args是可以传递给break以指定断点位置的任何参数。括着condition的圆括号是可选的。

    例如:

    break main if argc>1

    例如,在循环中,满足一定次数之后发生中断:

    break if (i==7000) 

    条件中断中的condition可以包含如下形式,但是必须是布尔值:

    可以对正常断点设置条件以将它转变为条件断点。例如,如果设置了断点3为无条件断点,但是希望添加添加i==3,只有键入:

    (gdb) cond 3 i==3

    如果以后要删除条件,但是保持该断点,只要键入:

    (gdb) cond 3

    8 断点命令列表

    当GDB遇到断点时,几乎总是要查看某个变量。如果反复遇到同一个断点,将反复查看相同的变量。让GDB在每次到达某个断点时自动执行一组命令,从而自动完成这一过程。

    事实上,使用“断点命令列表”就可以做这件事。

    使用commands命令设置命令列表。

    其中breakpoint-number是要将命令添加到其上的断点的标识符,commands是用行分隔的任何有效GDB命令列表。逐条输入命令,然后键入end表示输入命令完毕。从那以后,每当GDB在这个断点处中断时,它都会执行输入的任何命令。

    例如:

    fibonacci.c

    复制代码
    #include<stdio.h>
    int fibonacci(int n);
    
    int main(void)
    {
        printf("Fibonacci(3) is %d
    ",fibonacci(3));
    
        return 0;
    }
    
    int fibonacci(int n)
    {
        if(n<=0||n==1)
            return 1;
        else
            return fibonacci(n-1)+fibonacci(n-2);
    }
    复制代码

    gdb调试:

    如果觉得输出太冗长了,可以使用silent命令使GDB更安静地触发断点。

    现在输出结果不错,但是每次要键入continue,可以修改如下:

    也可以使用define定义宏来代替:

    9 监视点

    监视点是一种特殊类型的断点,它类似于正常断点,是要求GDB暂停程序执行的指令。监视点是指示GDB每当某个表达式改变了值就暂停执行的指令。

    (gdb) watch i

    它会使得每当i改变值时GDB就暂停。

    9.1 设置监视点

    当变量var存在且在作用域中时,可以通过使用如下命令来设置监视点

    watch  var

    该命令会导致每当var改变值时GDB都中断。

    例如:

    复制代码
    #include<stdio.h>
    int i=0;
    
    int main()
    {
        i=3;
        printf("i is %d.
    ",i);
    
        i=5;
        printf("i is %d.
    ",i);
    
        return 0;
    }
    复制代码

    我们每当i大于4时得到通知。因此在main()的入口处放一个断点,以便让i在作用域中,并设置一个监视点以指出i何时大于4.不能在i上设置监视点,因为在程序运行之前,i不存在。因此必须现在main()上设置断点,然后在i上设置监视点

    既然i已经在作用域中了,现在设置监视点并通知GDB继续执行程序。

    10 显示数值中的值

    比如声明数组:

    int x[25];

    方法是通过键入:

    (gdb) p x

    但是,如果是动态创建的数组会是什么样呢?比如:

    int *x

    ...

    x=(int *)malloc(25*sizeof(int));

    如果要在GDB中输出数组,就不能输入:

    (gdb) p x

    可以简单打印数组地址。或者键入:

    (gdb) p *x

    这样只会输出数组的一个元素——x[0]。仍然可以像在命令 p x[5]中那样输入单个元素,但是不能简单地在x上使用print命令输出整个数组。

    1)在GDB的解决方案

    在GDB中,可以通过创建一个人工数组来解决这个问题。如下:

    复制代码
    #include<stdio.h>
    #include<stdlib.h>
    int *x;
    void main()
    {
        x=(int*)malloc(25*sizeof(int));
        x[3]=12;
    }
    复制代码

    然后执行:

    我们可以看到,一般形式为:

    *pointer@number_of_elements

    GDB还允许在适当的时候使用类型强制转换,比如:

    (gdb) p (int [25])*x

    $2={0,0,0,12,0 <repeats 21 times>}

  • 相关阅读:
    使用新建项目,引用底层库,运行时提示http://www.xinxizhan.cn/none-authorization.html。无授权解决方法
    treeviewhelper用法,找child的UIElement
    用#FFFF2222的string生成color
    DataTemplate.LoadContent Method将resource中的datatemplate转换为UIElement,可以用于对象添加
    Server.Mappath
    回调函数
    用where进行条件查询
    NPOI用法。
    [转].tostring设置格式。C# tostring 格式化输出
    【转】灵活运用 SQL SERVER FOR XML PATH。用于方便处理生成视图
  • 原文地址:https://www.cnblogs.com/pugang/p/13070471.html
Copyright © 2020-2023  润新知