调试命令(二)

info 和 thread 命令

info thread

  • 可以用来查看当前进程的所有线程运行情况
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/gdbtest/redis-4.0.11/src/redis-server
[Thread debugging using libthread_db enabled]
...省略部分无关内容...
53062:M 10 Sep 17:11:10.810 * Ready to accept connections
^C
Program received signal SIGINT, Interrupt.
0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
(gdb) info thread
  Id   Target Id         Frame
  4    Thread 0x7fffef7fd700 (LWP 53065) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
  3    Thread 0x7fffefffe700 (LWP 53064) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
  2    Thread 0x7ffff07ff700 (LWP 53063) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
* 1    Thread 0x7ffff7fec780 (LWP 53062) "redis-server" 0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
  • 通过 info thread 的输出可以知道 redis-server 正常启动后,一共产生了 4个线程,包括一个主线程和三个工作线程,线程编号(Id 那一列)分别是 4、3、2、1。三个工作线程(2、3、4)分别阻塞在 Linux API pthread_cond_wait 处,而主线程(1)阻塞在epoll_wait 处。

  • 注意:虽然第一栏的名称叫 Id,但第一栏的数值不是线程的 Id,第三栏括号里的内容(如 LWP 53065)中,53065这样的数值才是当前线程真正的 Id。那 LWP 是什么意思呢?在早期的 Linux系统的内核里面,其实不存在真正的线程实现,当时所有的线程都是用进程来实现的,这些模拟线程的进程被称为 Light Weight Process(轻量级进程),后来 Linux 系统有了真正的线程实现,这个名字仍然被保留了下来。

  • 线程编号前面这个星号表示的是当前 GDB 作用于哪个线程,而不是主线程的意思。现在有 4 个线程,也就有 4 个调用堆栈,如果此时输入 backtrace 命令查看调用堆栈,由于当前 GDB 作用在线程 1,因此 backtrace 命令显示的一定是线程 1 的调用堆栈:

(gdb) bt
#0  0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
#1  0x00000000004265df in aeApiPoll (tvp=0x7fffffffe300, eventLoop=0x7ffff08350a0) at ae_epoll.c:112
#2  aeProcessEvents (eventLoop=eventLoop@entry=0x7ffff08350a0, flags=flags@entry=11) at ae.c:411
#3  0x0000000000426aeb in aeMain (eventLoop=0x7ffff08350a0) at ae.c:501
#4  0x00000000004238ef in main (argc=1, argv=0x7fffffffe648) at server.c:3899
  • 由此可见,堆栈 #4 的 main() 函数也证实了上面的说法,即线程编号为 1 的线程是主线程。

thread 线程编号

  • 如何切换到其他线程呢?可以通过“thread 线程编号”切换到具体的线程上去。例如,想切换到线程 2 上去,只要输入 thread 2 即可,然后输入 bt 就能查看这个线程的调用堆栈了:
(gdb) info thread
  Id   Target Id         Frame
  4    Thread 0x7fffef7fd700 (LWP 53065) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
  3    Thread 0x7fffefffe700 (LWP 53064) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
  2    Thread 0x7ffff07ff700 (LWP 53063) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
* 1    Thread 0x7ffff7fec780 (LWP 53062) "redis-server" 0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
(gdb) thread 2
[Switching to thread 2 (Thread 0x7ffff07ff700 (LWP 53063))]
#0  0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
(gdb) bt
#0  0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
#1  0x000000000047a91c in bioProcessBackgroundJobs (arg=0x0) at bio.c:176
#2  0x00007ffff76c0e25 in start_thread () from /lib64/libpthread.so.0
#3  0x00007ffff73ee34d in clone () from /lib64/libc.so.6
  • 注意,当把 GDB 当前作用的线程切换到线程 2 上之后,线程 2 前面就被加上了星号,如下:
(gdb) info thread
  Id   Target Id         Frame
  4    Thread 0x7fffef7fd700 (LWP 53065) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
  3    Thread 0x7fffefffe700 (LWP 53064) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
* 2    Thread 0x7ffff07ff700 (LWP 53063) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
  1    Thread 0x7ffff7fec780 (LWP 53062) "redis-server" 0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6

info args

  • info 命令还可以用来查看当前函数的参数值,组合命令是 info args,我们找个函数值多一点的堆栈函数来试一下:
(gdb) thread 1
[Switching to thread 1 (Thread 0x7ffff7fec780 (LWP 53062))]
#0  0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
(gdb) bt
#0  0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
#1  0x00000000004265df in aeApiPoll (tvp=0x7fffffffe300, eventLoop=0x7ffff08350a0) at ae_epoll.c:112
#2  aeProcessEvents (eventLoop=eventLoop@entry=0x7ffff08350a0, flags=flags@entry=11) at ae.c:411
#3  0x0000000000426aeb in aeMain (eventLoop=0x7ffff08350a0) at ae.c:501
#4  0x00000000004238ef in main (argc=1, argv=0x7fffffffe648) at server.c:3899
(gdb) f 2
#2  aeProcessEvents (eventLoop=eventLoop@entry=0x7ffff08350a0, flags=flags@entry=11) at ae.c:411
411             numevents = aeApiPoll(eventLoop, tvp);
(gdb) info args
eventLoop = 0x7ffff08350a0
flags = 11
(gdb)
  • 上述代码片段切回至主线程 1,然后切换到堆栈 #2,堆栈 #2 调用处的函数是 aeProcessEvents() ,一共有两个参数,使用 info args 命令可以输出当前两个函数参数的值,参数 eventLoop 是一个指针类型的参数,对于指针类型的参数,GDB 默认会输出该变量的指针地址值,如果想输出该指针指向对象的值,在变量名前面加上 * 解引用即可,这里使用 p *eventLoop 命令:
gdb) p *eventLoop
$26 = {maxfd = 11, setsize = 10128, timeEventNextId = 1, lastTime = 1536570672, events = 0x7ffff0871480, fired = 0x7ffff08c2e40, timeEventHead = 0x7ffff0822080,
  stop = 0, apidata = 0x7ffff08704a0, beforesleep = 0x429590 <beforeSleep>, aftersleep = 0x4296d0 <afterSleep>}
  • 从如果还要查看其成员值,继续使用 变量名 ->字段名 即可。

help info

(gdb) help info
Generic command for showing things about the program being debugged.

List of info subcommands:

info address -- Describe where symbol SYM is stored
info all-registers -- List of all registers and their contents
info args -- Argument variables of current stack frame
info auto-load -- Print current status of auto-loaded files
info auto-load-scripts -- Print the list of automatically loaded Python scripts
info auxv -- Display the inferior's auxiliary vector
info bookmarks -- Status of user-settable bookmarks
info breakpoints -- Status of specified breakpoints (all user-settable breakpoints if no argument)
info checkpoints -- IDs of currently known checkpoints
info classes -- All Objective-C classes

next、step、until、finish、return 和 jump 命令

  • 这几个命令是我们用 GDB 调试程序时最常用的几个控制流命令,因此放在一起介绍。

next

  • next 命令(简写为 n)是让 GDB 调到下一条命令去执行,这里的下一条命令不一定是代码的下一行,而是根据程序逻辑跳转到相应的位置。
int a = 0;
if (a == 9)
{
    print("a is equal to 9.\n");
}

int b = 10;
print("b = %d.\n", b);
  • 如果当前 GDB 中断在上述代码第 2 行,此时输入 next 命令 GDB 将调到第 7 行,因为这里的 if 条件并不满足。

技巧:在 GDB 命令行界面如果直接按下回车键,默认是将最近一条命令重新执行一遍,因此,当使用 next 命令单步调试时,不必反复输入 n 命令,直接回车就可以了。

3704    int main(int argc, char **argv) {
(gdb) n
3736        spt_init(argc, argv);
(gdb) n
3738        setlocale(LC_COLLATE,"");
(gdb) n
3739        zmalloc_set_oom_handler(redisOutOfMemoryHandler);
(gdb) n
3740        srand(time(NULL)^getpid());
(gdb) n
3752        server.exec_argv = zmalloc(sizeof(char*)*(argc+1));
(gdb) n
3740        srand(time(NULL)^getpid());
(gdb) n
3741        gettimeofday(&tv,NULL);
(gdb) n
3752        server.exec_argv = zmalloc(sizeof(char*)*(argc+1));
(gdb)
  • 上面的执行过程等价于输入第一个 n 后直接回车:
(gdb) n
3736        spt_init(argc, argv);
(gdb)
3738        setlocale(LC_COLLATE,"");
(gdb)
3739        zmalloc_set_oom_handler(redisOutOfMemoryHandler);
(gdb)
3740        srand(time(NULL)^getpid());
(gdb)
3752        server.exec_argv = zmalloc(sizeof(char*)*(argc+1));
(gdb)

next 命令用调试的术语叫“单步步过”(step over),即遇到函数调用直接跳过,不进入函数体内部。而下面的 step 命令(简写为 s)就是“单步步入”(step into),顾名思义,就是遇到函数调用,进入函数内部。
step

  • 举个例子,在 redis-server 的 main() 函数中有个叫 spt_init(argc, argv) 的函数调用,当我们停在这一行时,输入 s 将进入这个函数内部。
int main(int argc, char **argv) {
    struct timeval tv;
    int j;
    /* We need to initialize our libraries, and the server configuration. */
    spt_init(argc, argv);
    setlocale(LC_COLLATE,"");
    zmalloc_set_oom_handler(redisOutOfMemoryHandler);
    srand(time(NULL)^getpid());
    gettimeofday(&tv,NULL);
    char hashseed[16];
    getRandomHexChars(hashseed,sizeof(hashseed));
    dictSetHashFunctionSeed((uint8_t*)hashseed);
    server.sentinel_mode = checkForSentinelMode(argc,argv);
    initServerConfig();
    moduleInitModulesSystem();
    //省略部分无关代码...
 }
  • 先使用 b main 命令在 main() 处加一个断点,然后使用 r 命令重新跑一下程序,会触发刚才加在 main() 函数处的断点,然后使用 n 命令让程序走到 spt_init(argc, argv) 函数调用处,再输入 s 命令就可以进入该函数了
(gdb) b main
Breakpoint 3 at 0x423450: file server.c, line 3704.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/redis-4.0.9/src/redis-server
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 3, main (argc=1, argv=0x7fffffffe588) at server.c:3704
3704    int main(int argc, char **argv) {
(gdb) n
3736        spt_init(argc, argv);
(gdb) s
spt_init (argc=argc@entry=1, argv=argv@entry=0x7fffffffe588) at setproctitle.c:152
152     void spt_init(int argc, char *argv[]) {
(gdb) l
147
148             return 0;
149     } /* spt_copyargs() */
150
151
152     void spt_init(int argc, char *argv[]) {
153             char **envp = environ;
154             char *base, *end, *nul, *tmp;
155             int i, error;
156
(gdb)
  • 有一个需要注意的地方,就是当函数的参数也是函数调用时,我们使用 step 命令会依次进入各个函数,那么顺序是什么呢?举个例子,看下面这段代码:
int fun1(int a, int b)
2  {
3     int c = a + b;
4     c += 2;
5     return c;
6  }
7
8  int func2(int p, int q)
9  {
10    int t = q * p;
11       return t * t;
12 }
13
14 int func3(int m, int n)
15 {
16    return m + n;
17 }
18
19 int main()
20 {
21    int c;
22    c = func3(func1(1, 2),  func2(8, 9));
23    printf("c=%d.\n", c);
24    return 0;
25 }
  • 上述代码,程序入口是 main() 函数,在第 22 行 func3 使用 func1 和 func2 的返回值作为自己的参数,在第 22 行输入 step 命令,会先进入哪个函数呢?这里就需要补充一个知识点了—— 函数调用方式,我们常用的函数调用方式有 _cdecl_stdcallC++ 非静态成员函数的调用方式是 _thiscall 。在这些调用方式中,函数参数的传递本质上是函数参数的入栈过程,而这三种调用方式参数的入栈顺序都是从右往左的,因此,这段代码中并没有显式标明函数的调用方式,采用默认 _cdecl 方式。

  • 当我们在第 22 行代码处输入 step 先进入的是 func2() ,当从 func2() 返回时再次输入 step 命令会接着进入 func1() ,当从 func1 返回时,此时两个参数已经计算出来了,这时候会最终进入 func3() 。理解这一点,在遇到这样的代码时,才能根据需要进入我们想要的函数中去调试。

  • 实际调试时,我们在某个函数中调试一段时间后,不需要再一步步执行到函数返回处,希望直接执行完当前函数并回到上一层调用处,就可以使用 finish 命令。与 finish 命令类似的还有 return 命令,return 命令的作用是结束执行当前函数,还可以指定该函数的返回值。

  • 这里需要注意一下二者的区别:

    • finish 命令会执行函数到正常退出该函数
    • return 命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了。
#include <stdio.h>
2
3  int func()
4  {
5     int a = 9;
6     printf("a=%d.\n", a);
7
8     int b = 8;
9     printf("b=%d.\n", b);
10    return a + b;
11 }
12
13 int main()
14 {
15    int c = func();
16    printf("c=%d.\n", c);
17
18    return 0;
19 }
  • 在 main() 函数处加一个断点,然后运行程序,在第 15 行使用 step 命令进入 func() 函数,接着单步到代码第 8 行,直接输入 return 命令,这样 func() 函数剩余的代码就不会继续执行了,因此 printf(“b=%d.\n”, b); 这一行就没有输出。同时由于我们没有在 return 命令中指定这个函数的返回值,因而最终在 main() 函数中得到的变量 c 的值是一个脏数据。这也就验证了我们上面说的:return 命令在当前位置立即结束当前函数的执行,并返回到上一层调用。

return

(gdb) b main
Breakpoint 1 at 0x40057d: file test.c, line 15.
(gdb) r
Starting program: /root/testreturn/test

Breakpoint 1, main () at test.c:15
15          int c = func();
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64
(gdb) s
func () at test.c:5
5           int a = 9;
(gdb) n
6           printf("a=%d.\n", a);
(gdb) n
a=9.
8           int b = 8;
(gdb) return
Make func return now? (y or n) y
#0  0x0000000000400587 in main () at test.c:15
15          int c = func();
(gdb) n
16          printf("c=%d.\n", c);
(gdb) n
c=-134250496.
18          return 0;
(gdb)
  • 用 return 命令指定一个值试一下,这样得到变量 c 的值应该就是我们指定的值。
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/testreturn/test

Breakpoint 1, main () at test.c:15
15          int c = func();
(gdb) s
func () at test.c:5
5           int a = 9;
(gdb) n
6           printf("a=%d.\n", a);
(gdb) n
a=9.
8           int b = 8;
(gdb) return 9999
Make func return now? (y or n) y
#0  0x0000000000400587 in main () at test.c:15
15          int c = func();
(gdb) n
16          printf("c=%d.\n", c);
(gdb) n
c=9999.
18          return 0;
(gdb) p c
$1 = 9999
(gdb)
  • 仔细观察上述代码应该会发现,用 return 命令修改了函数的返回值,当使用 print 命令打印 c 值的时候,c 值也确实被修改成了 9999。

finish

  • 使用 finish 命令来结束函数执行的结果
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/testreturn/test

Breakpoint 1, main () at test.c:15
15          int c = func();
(gdb) s
func () at test.c:5
5           int a = 9;
(gdb) n
6           printf("a=%d.\n", a);
(gdb) n
a=9.
8           int b = 8;
(gdb) finish
Run till exit from #0  func () at test.c:8
b=8.
0x0000000000400587 in main () at test.c:15
15          int c = func();
Value returned is $3 = 17
(gdb) n
16          printf("c=%d.\n", c);
(gdb) n
c=9999.
18          return 0;
(gdb)
  • 结果和我们预期的一样,finish 正常结束函数,剩余的代码也会被正常执行。因此 c 的值是 17。

until

  • until 命令(简写为 u)可以指定程序运行到某一行停下来
void initServer(void) {
1813        int j;
1814
1815        signal(SIGHUP, SIG_IGN);
1816        signal(SIGPIPE, SIG_IGN);
1817        setupSignalHandlers();
1818
1819        if (server.syslog_enabled) {
1820            openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT,
1821                server.syslog_facility);
1822        }
1823
1824        server.pid = getpid();
1825        server.current_client = NULL;
1826        server.clients = listCreate();
1827        server.clients_to_close = listCreate();
1828        server.slaves = listCreate();
1829        server.monitors = listCreate();
1830        server.clients_pending_write = listCreate();
1831        server.slaveseldb = -1; /* Force to emit the first SELECT command. */
1832        server.unblocked_clients = listCreate();
1833        server.ready_keys = listCreate();
1834        server.clients_waiting_acks = listCreate();
1835        server.get_ack_from_slaves = 0;
1836        server.clients_paused = 0;
1837        server.system_memory_size = zmalloc_get_memory_size();
1838
1839        createSharedObjects();
1840        adjustOpenFilesLimit();
1841        server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
1842        if (server.el == NULL) {
1843            serverLog(LL_WARNING,
1844                "Failed creating the event loop. Error message: '%s'",
1845                strerror(errno));
1846            exit(1);
1847        }
  • 这是 redis-server 代码中 initServer() 函数的一个代码片段,位于文件 server.c 中,当停在第 1813 行,想直接跳到第 1839 行,可以直接输入 u 1839这样就能快速执行完中间的代码。当然,也可以先在第 1839 行加一个断点,然后使用 continue 命令运行到这一行,但是使用 until 命令会更简便。
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/redis-4.0.9/src/redis-server
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 3, main (argc=1, argv=0x7fffffffe588) at server.c:3704
3704    int main(int argc, char **argv) {
(gdb) c
Continuing.
21574:C 14 Sep 06:42:36.978 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
21574:C 14 Sep 06:42:36.978 # Redis version=4.0.9, bits=64, commit=00000000, modified=0, pid=21574, just started
21574:C 14 Sep 06:42:36.979 # Warning: no config file specified, using the default config. In order to specify a config file use /root/redis-4.0.9/src/redis-server /path/to/redis.conf

Breakpoint 4, initServer () at server.c:1812
1812    void initServer(void) {
(gdb) n
1815        signal(SIGHUP, SIG_IGN);
(gdb) u 1839
initServer () at server.c:1839
1839        createSharedObjects();
(gdb)

jump

  • 基本用法是:jump <location>
  • location 可以是程序的行号或者函数的地址,jump 会让程序执行流跳转到指定位置执行,当然其行为也是不可控制的,例如您跳过了某个对象的初始化代码,直接执行操作该对象的代码,那么可能会导致程序崩溃或其他意外行为。jump 命令可以简写成 j,但是不可以简写成 jmp,
  • 其使用有一个注意事项,即如果 jump 跳转到的位置后续没有断点,那么 GDB 会执行完跳转处的代码会继续执行
1 int somefunc()
2 {
3   //代码A
4   //代码B
5   //代码C
6   //代码D
7   //代码E
8   //代码F
9 }
  • 假设我们的断点初始位置在行号 3 处(代码 A),这个时候我们使用 jump 6,那么程序会跳过代码 B 和 C 的执行,执行完代码 D( 跳转点),程序并不会停在代码 6 处,而是继续执行后续代码,因此如果我们想查看执行跳转处的代码后的结果,需要在行号 6、7 或 8 处设置断点。

  • jump 命令除了跳过一些代码的执行外,还有一个妙用就是可以执行一些我们想要执行的代码,而这些代码在正常的逻辑下可能并不会执行(当然可能也因此会产生一些意外的结果,这需要读者自行斟酌使用)。

#include <stdio.h>
2  int main()
3  {
4    int a = 0;
5    if (a != 0)
6    {
7      printf("if condition\n");
8    }
9    else
10   {
11     printf("else condition\n");
12   }
13
14   return 0;
15 }
  • 我们在行号 4 、14 处设置一个断点,当触发行号 4 处的断点后,正常情况下程序执行流会走 else 分支,我们可以使用 jump 7 强行让程序执行 if 分支,接着 GDB 会因触发行号 14 处的断点而停下来,此时我们接着执行 jump 11,程序会将 else 分支中的代码重新执行一遍。整个操作过程如下:
[root@localhost testcore]# gdb test
Reading symbols from /root/testcore/test...done.
(gdb) b main
Breakpoint 1 at 0x400545: file main.cpp, line 4.
(gdb) b 14
Breakpoint 2 at 0x400568: file main.cpp, line 14.
(gdb) r
Starting program: /root/testcore/test

Breakpoint 1, main () at main.cpp:4
4       int a = 0;
Missing separate debuginfos, use: debuginfo-install glibc-2.17-260.el7.x86_64 libgcc-4.8.5-36.el7.x86_64 libstdc++-4.8.5-36.el7.x86_64
(gdb) jump 7
Continuing at 0x400552.
if condition

Breakpoint 2, main () at main.cpp:14
14       return 0;
(gdb) jump 11
Continuing at 0x40055e.
else condition

Breakpoint 2, main () at main.cpp:14
14       return 0;
(gdb) c
Continuing.
[Inferior 1 (process 13349) exited normally]
(gdb)
  • 本质上,jump 命令的作用类似于在 Visual Studio 中调试时,拖鼠标将程序从一个执行处拖到另外一个执行处。
    在这里插入图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值