43、调试技巧与GDB使用全解析

调试技巧与GDB使用全解析

在软件开发过程中,调试是一项至关重要的技能。本文将深入探讨调试过程中的一些关键问题,包括 printf 调试的副作用,以及强大的GNU调试器 gdb 的使用方法。

1. printf 调试的副作用

在调试过程中, printf 是一种常用的调试手段,但它并非没有副作用。其中一个未被提及的副作用是意外同步,这在多线程代码中更为常见,但在单线程代码中也可能出现。

例如,你可能遇到过这样的情况:当打开 printf 时,某个bug消失了。在多线程应用中,一个巧妙放置的 printf 可能会隐藏竞态条件。而在单线程应用中, printf 可能会导致编译器将原本存储在寄存器中的浮点数存储到内存中。由于IA32上的浮点寄存器比IEEE浮点数具有更高的精度,添加 printf 可能会改变数值结果。

以下是一个示例代码 side-effects.c ,展示了 printf 在IA32上的副作用:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>

int main(int argc, char *argv[])
{
    // Make argument a volatile variable. This prevents
    // the optimizer from taking any shortcuts.
    volatile double arg = 7.0;

    // Square root of a prime is 'irrational';
    // i.e. digits go on forever. On IA32, x
    // will be in an 88-bit floating point register.
    double x = sqrt(arg);

#ifndef NOPRINT
    // If we print it, x must be stored on the stack.
    // double has only 64-bits so we lose precision
    printf("x    = % 0.20f\n", x);
#endif

    // By calling printf, we changed the value of x,
    // which will show up as a non-zero diff.
    volatile double diff = sqrt(arg) - x;
    printf("diff = % 0.20f\n", diff);

    if (diff == 0.0) {
        printf("Zero diff!\n");
    }
    else {
        printf("Nonzero diff!!!\n");
    }
}

编译该程序的命令如下:

$ cc -o print   -O2           -lm side-effects.c
$ cc -o noprint -O2 -DNOPRINT -lm side-effects.c

运行结果如下:

$ ./noprint
diff =  0.00000000000000000000
Zero diff!
$ ./print
x    =  2.64575131106459071617
diff = -0.00000000000000012577
Nonzero diff!!!

从上述代码和运行结果可以看出,仅仅调用 printf 就足以改变程序的行为,从而改变结果。这是因为它迫使编译器将变量 x 存储在内存中,而内存的精度低于IA32上的内部浮点寄存器。

为了避免这种错误,可以使用 -ffloat-store 选项来强制编译器不使用浮点寄存器进行存储。

2. 熟悉GNU调试器: gdb

gdb 是一个基于文本的调试器,具有直观的命令。大多数Linux程序员至少有过一些使用它的经验。 gdb 的每个命令都可以缩写,有些甚至只需要一个字母。通常,你只需要输入命令的前几个字母, gdb 会根据你输入的字母唯一识别命令。如果你输入的字母不够, gdb 会给出有用的提示。

例如,在 gdb 提示符下输入 sh ,会看到以下消息:

(gdb) sh
Ambiguous command "sh": sharedlibrary, shell, show.

输入几个字母后,你可以按 Tab 键,如果可能的话,它会根据你已经输入的字母补全命令。如果没有提供补全选项,再次按 Tab 键可以查看匹配的命令列表。例如:

(gdb) b<Tab><Tab>
backtrace  break      bt
(gdb) b

这里输入 b 后, gdb 提供了 backtrace break bt 作为可能的补全选项。

2.1 使用 gdb 运行代码

启动 gdb 时,你可以在命令行中指定要调试的程序,也可以先启动 gdb ,然后加载程序。以下两种方式都可行:

$ gdb ./hello

或者

$ gdb
(gdb) file ./hello
Reading symbols from /home/john/hello...done.

这两个命令都会将你的文件及其符号读入内存。后一种方式还允许你在不退出 gdb 的情况下切换程序。

gdb 提示符下,你可以使用 run 命令进入代码,这在你已经知道程序会崩溃时很有用。当程序崩溃时,你可以获取堆栈回溯信息,以查看代码崩溃的位置。更多时候,你可能想逐步执行代码或设置断点。

以下是在 gdb 中启动代码所需了解的基本命令:
- set args set 命令的特殊情况,用于传递命令行参数。 gdb 会存储你指定的参数,供后续的 run 命令使用,不过你也可以在每次调用 run 时指定新的参数。要清除已设置的参数,必须使用 set args
- run :从程序的开头开始执行。程序将使用这里指定的参数或最近一次调用 set args 设置的参数执行。执行将继续,直到程序退出、中止或遇到断点。
- start :与 run 类似,但程序会在 main 函数处停止,就像设置了一个断点一样。这允许你从程序的开头单步执行。
- step :执行一行代码。如果该行包含一个已使用调试信息编译的函数, gdb 会进入该函数。在使用此命令之前,你必须使用 run start 命令启动程序。
- next :与 step 类似,但 gdb 不会进入函数,无论函数是否使用调试信息编译。
- kill :终止程序。这与 kill 系统调用不同, kill 系统调用会向正在运行的程序发送信号。要发送信号,请使用 signal 命令。

以下是这些命令的流程图:

graph LR
    A[启动gdb] --> B{指定程序方式}
    B -->|命令行指定| C(gdb ./hello)
    B -->|先启动后加载| D(gdb; file ./hello)
    C --> E{运行方式}
    D --> E
    E -->|run| F(程序从头执行)
    E -->|start| G(程序在main处停止)
    F --> H{执行操作}
    G --> H
    H -->|step| I(执行一行代码,进入函数)
    H -->|next| J(执行一行代码,不进入函数)
    H -->|kill| K(终止程序)
    H -->|set args| L(设置命令行参数)
2.2 停止和重新启动执行

你可以随时按 Ctrl+C 停止正在运行的程序。这会告诉 gdb 停止当前正在运行的程序,但不会向程序发送 SIGINT 信号,因此你可以从程序停止的位置继续执行。有些应用程序适合这种无断点调试方式,但大多数应用程序不适合。对于不适合的应用程序, gdb 提供了显式的断点命令。

以下是基本的命令:
- break , tbreak :设置断点( break )或临时断点( tbreak )。如果没有参数,命令将在当前要执行的下一条指令处设置断点;更多时候,你会指定一个函数或行号来停止程序。这些命令后面还可以跟一个逻辑表达式,以创建条件断点。
- watch :类似于断点,但使用硬件寄存器(如果可用)来监视特定位置的变化。这可以使程序的执行速度比传统断点快得多。
- continue :从程序停止的位置继续执行。 continue 命令允许一个可选的数字参数(N),告诉 gdb 忽略接下来的N - 1个断点。换句话说,“继续执行,直到程序第N次遇到断点”。
- signal :向程序发送信号并继续执行。该命令接受一个参数,可以是信号编号或信号名称。你可以通过输入 info signal 查看信号名称列表。
- info breakpoints :列出当前活动的断点和观察点。
- delete :删除所有断点。要删除单个断点,使用 info breakpoints 获取断点编号,然后将该编号传递给 delete 命令。

以下是这些命令的使用示例表格:
| 命令 | 作用 | 示例 |
| — | — | — |
| break | 设置断点 | (gdb) break foo |
| tbreak | 设置临时断点 | (gdb) tbreak bar |
| watch | 设置观察点 | (gdb) watch variable |
| continue | 继续执行 | (gdb) continue |
| signal | 发送信号 | (gdb) signal SIGTERM |
| info breakpoints | 列出断点 | (gdb) info breakpoints |
| delete | 删除断点 | (gdb) delete 1 |

2.3 断点语法

断点命令通常缩写为 b ,因为它使用非常频繁。断点命令的参数始终是一个指令地址,可以通过以下方式提供:
- (gdb) b :在当前堆栈帧的下一条指令处中断。
- (gdb) b foo :在函数 foo 处中断。
- (gdb) b foobar.c:foo :在 foobar.c 文件中定义的函数 foo 处中断。
- (gdb) b 10 :在当前模块的第10行处中断。
- (gdb) b foobar.c:10 :在 foobar.c 文件的第10行处中断。
- (gdb) b *0xdeadbeef :在地址 0xdeadbeef 处中断。

以下是一个有缺陷的程序 nasty.c ,它会根据请求溢出堆:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void *nasty(char *buf, int setlen)
{
    // We don't check setlen! Naughty!
    return memset(buf, 'a', setlen);
}

// Use a fixed buffer length...
const int buflen = 16;

int main(int argc, char *argv[])
{
    char *buf = malloc(buflen);

    // Default to same length as buffer
    int len = buflen;
    if (argc > 1)
        // Allow command line arguments to override buffer length.
        len = atoi(argv[1]);

    // If len > buflen, then this corrupts the heap.
    nasty(buf, len);

    // Some versions of glibc detect errors here, but not always.
    free(buf);

    // Get here and everything should be okay.
    printf("buflen=%d len=%d okay\n", buflen, len);
    return 0;
}

在这个程序中,我们可以在 memset 函数上设置断点,即使 memset 是标准库的一部分,没有使用调试信息编译。调试会话如下:

$ gdb ./nasty
GNU gdb Red Hat Linux (6.1post-1.20040607.43.0.1rh)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-redhat-linux-gnu"...Using host libthread_db
library "/lib/tls/libthread_db.so.1".
(gdb) b memset
Function "memset" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Breakpoint 1 (memset) pending.
(gdb) run 100
Starting program: /home/john/examples/ch-10/debug/nasty 100
Reading symbols from shared object read from target memory...done.
Loaded system supplied DSO at 0xffffe000
Breakpoint 2 at 0xb7e4e050
Pending breakpoint "memset" resolved
Breakpoint 2, 0xb7e4e050 in memset () from /lib/tls/libc.so.6
(gdb) bt
#0  0xb7e4e050 in memset () from /lib/tls/libc.so.6
#1  0x0804844e in nasty (buf=0x804a008 "", setlen=100) at nasty.c:8
#2  0x080484b5 in main (argc=2, argv=0xbf836564) at nasty.c:25

这个会话说明了几个概念:首先,当加载程序时,共享库可能尚未加载,因此 gdb 可能不识别某些符号。 gdb 会提示你设置待决断点,当共享库加载时,它会查找该符号并设置断点。使用 run 命令启动程序,程序会在 memset 处停止,使用 bt 命令可以查看调用栈。

2.4 使用条件断点

对于 nasty.c 程序,我们还可以使用条件断点进行调试。例如,当 setlen 大于 buflen 时,在 nasty 函数处停止。语法如下:

(gdb) b nasty if setlen > buflen
Breakpoint 1 at 0x804843e: file nasty.c, line 8.
(gdb) run 100
Starting program: /home/john/examples/ch-10/debug/nasty 100
Reading symbols from shared object read from target memory...done.
Loaded system supplied DSO at 0xffffe000
Breakpoint 1, nasty (buf=0x804a008 "", setlen=100) at nasty.c:8
8           return memset(buf, 'a', setlen);

条件断点中使用的变量必须与断点地址在同一作用域内。例如,以下条件断点将不起作用:

(gdb) b nasty if len > buflen
No symbol "len" in current context.

因为 len main 函数内部的局部变量,在调用 nasty 函数时不在作用域内。你可以使用C++风格的作用域操作符显式指定作用域:

(gdb) b nasty if main::len > buflen
Breakpoint 1 at 0x804845e: file nasty.c, line 8.

需要注意的是,即使代码不是用C++编写的,也可以使用这种语法。

2.5 使用C++代码设置断点

调试C++程序可能具有挑战性,因为命名空间、重载和模板的存在,很难确定断点的符号。幸运的是, gdb 提供了一些有用的快捷方式来简化调试过程。

以下是一个C++程序 cppsym.c

// Three inconveniently named functions
// wrapped inside a namespace, just to make them more annoying.
// And for good measure, we overload one of the functions.

namespace inconvenient {
    void *annoyingFunctionName1(void *ptr) {
        return ptr;
    };
    void *annoyingFunctionName2(void *ptr) {
        return ptr;
    };
    void *annoyingFunctionName3(void *ptr) {
        return ptr;
    };
    void *annoyingFunctionName3(int x) {
        return (void *) x;
    };
};

// Too bad the 'using' statement is not an option in gdb...
using namespace inconvenient;

int main(int argc, char *argv[])
{
    annoyingFunctionName1(0);
    annoyingFunctionName2(0);
    annoyingFunctionName3(0);
    annoyingFunctionName3((int) 0);
}

由于函数名很长且相似,并且都位于命名空间中,使用 gdb 设置断点可能会很麻烦。不过, gdb 允许对所有命令和符号进行 Tab 补全,这有助于减少输入量并避免输入错误。

首先,使用 info function 命令查找包含“annoy”的函数:

(gdb) info function  annoy
Look for any function with the word “annoy” in it.
All functions matching regular expression "annoy":
File cppsym.cpp:
void *inconvenient::annoyingFunctionName1(void*);
void *inconvenient::annoyingFunctionName2(void*);
void *inconvenient::annoyingFunctionName3(int);
void *inconvenient::annoyingFunctionName3(void*);

这会显示命名空间和所有匹配的函数名。知道命名空间后,你可以使用 Tab 补全设置断点,但需要注意的是, gdb Tab 补全不包含命名空间中的冒号。因此,你需要在函数名前加上单引号,然后使用 Tab 补全,例如:

(gdb) b 'inc<Tab><Tab>
inconvenient
inconvenient::annoyingFunctionName1(void*)
inconvenient::annoyingFunctionName2(void*)
inconvenient::annoyingFunctionName3(int)
inconvenient::annoyingFunctionName3(void*)

Tab 键可以补全到第一个冒号,再次按 Tab 键会显示可能的匹配列表。要继续使用 Tab 补全,你需要手动输入两个冒号,然后继续使用 Tab 补全。最后,选择你想要的函数,关闭引号并按 Enter 键。完整的命令如下:

(gdb) b 'inconvenient::annoyingFunctionName3(void*)'
Breakpoint 2 at 0x804836f: file cppsym.cpp, line 13.

通过这种方式, Tab 补全大大减少了输入量。

2.6 使用观察点

许多处理器都配备了特殊用途的寄存器,用于辅助断点调试。 gdb 通过观察点命令使这些寄存器可供你使用。观察点允许你在程序读取或写入特定内存位置时停止程序。与断点不同,断点以指令地址作为参数,在该位置的代码执行时停止程序。观察点在查找由有缺陷的代码导致的内存损坏时特别有用。

对于不支持硬件观察点的架构, gdb 会单步执行可执行文件,并在每一步监视内存。这会导致代码的运行速度比正常情况慢几个数量级。

要设置一个观察点,以便在程序更改名为 foo 的变量的值时停止程序,只需使用以下命令:

(gdb) watch foo
Watch the value of foo for changes.

需要注意的是,观察点仅在内存中的值发生变化时触发。例如,如果 foo 的初始值为123,代码再次写入123,观察点不会触发。此外, watch 命令会自动将 foo 的地址用作观察点。如果 foo 是一个指针,你想要监视它所指向的位置,需要使用以下语法:

(gdb) watch *foo
Watch the location pointed to by foo for changes.

如果忘记使用星号,你将最终监视指针的值。

观察点可以与逻辑条件结合使用,创建条件观察点。例如,当 foo 被写入值123时停止程序:

(gdb) watch foo if foo == 123

这种语法与前面讨论的条件断点语法相同。条件表达式不一定要包含被监视的值,例如:

(gdb) watch foo if someflag == true

只要在观察点触发时满足所有作用域要求,任何逻辑语句都可以使用。由于观察点可以在代码的任何位置触发, gdb 不会假设条件语句中变量的作用域,并且在设置观察点时不会检查它们的作用域。如果条件表达式中的变量在观察点触发时不在作用域内,观察点将不会触发。

以下是一个更详细的示例,展示观察点的实用性。 overrun.c 文件包含一个有缺陷的程序,它有时会溢出堆缓冲区:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

// Source text for copying
const char text[] = "0123456789abcdef";

// This function will overrun if you tell it to.
void ovrrun(char *buf, const char *msg, int msglen)
{
    // Pointless memcpy - just to slow us down and illustrate
    // the usefulness of watchpoints
    char dummy[4096];
    memset(dummy, msglen, sizeof(dummy));

    // Here's the culprit...
    memcpy(buf, msg, msglen);
}

// Carefully chosen malloc size.
// malloc a small buffer so that space comes from the heap (not mmap).
// malloc will also pad the buffer, which means that a one-byte overrun
// should not cause the program to crash.
const int buflen = 13;

int main(int argc, char *argv[])
{
    char *buf = malloc(buflen);
    int i;
    // 后续代码可能还有其他逻辑,这里省略
}

在这个程序中, ovrrun 函数是一个围绕 memcpy 的简单包装器,由于没有进行边界检查,存在溢出目标缓冲区的风险。为了模拟一个处理密集型程序,添加了一个 memcpy 操作来减慢速度。目标缓冲区从堆中分配,大小为 buflen 。程序人为地创建了一个1/800,000的机会溢出目标缓冲区1个字节。由于 malloc 函数通常会进行填充,这种溢出通常不会产生副作用。你可以在事后使用 strlen 检测溢出,但通常为时已晚。如果溢出非常大,可能会导致程序崩溃。

如果不使用观察点,你可能会首先想到设置条件断点。例如,当 ovrrun 函数的 msglen 参数大于 buflen 时停止程序:

(gdb) b ovrrun if msglen > buflen

这个方法可以正常工作,但速度极慢。原因是每次调用 msglen 时,程序都会停止并将控制权转移给 gdb gdb 会每次检查 msglen 的值,并与 buflen 进行比较,决定是否继续或停止程序。由于这个程序会调用 ovrrun 函数800,000次,条件断点的开销会显著影响性能。在1.7 GHz的P4处理器上,没有设置断点时,运行该程序大约需要700毫秒;而设置条件断点后,程序运行时间超过2分17秒。

使用观察点则不会对代码产生任何影响,代码运行速度与没有设置观察点时一样快。设置观察点的命令如下:

(gdb) watch buf[buflen]

这里,你正在查找对 buf + buflen 位置的写入操作,这将表明发生了溢出。之所以速度如此之快,是因为触发操作由处理器硬件控制,处理器在写入操作发生之前不会生成触发信号。因此, gdb 只会停止程序一次,而不是800,000次。

观察点有三种类型:
- watch :当程序写入该位置且值发生变化时停止。
- rwatch :当程序读取该位置时停止。
- awatch :当程序读取或写入该位置时停止。

gdb 管理观察点的方式与管理断点相同。可以使用 info watchpoints 命令列出观察点(该命令与 info breakpoints 同义),也可以使用 delete 命令删除观察点。

综上所述, printf 调试虽然方便,但存在副作用,使用时需要谨慎。而 gdb 作为一个强大的调试工具,提供了丰富的功能,包括断点设置、条件断点、观察点等,可以帮助开发者更高效地调试程序。通过掌握这些调试技巧和 gdb 的使用方法,开发者能够更快地定位和解决程序中的问题,提高软件开发的质量和效率。

调试技巧与GDB使用全解析

3. 观察点的详细应用示例

为了更深入地理解观察点的实用性,我们继续分析 overrun.c 程序。以下是完整的 overrun.c 代码:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

// Source text for copying
const char text[] = "0123456789abcdef";

// This function will overrun if you tell it to.
void ovrrun(char *buf, const char *msg, int msglen)
{
    // Pointless memcpy - just to slow us down and illustrate
    // the usefulness of watchpoints
    char dummy[4096];
    memset(dummy, msglen, sizeof(dummy));

    // Here's the culprit...
    memcpy(buf, msg, msglen);
}

// Carefully chosen malloc size.
// malloc a small buffer so that space comes from the heap (not mmap).
// malloc will also pad the buffer, which means that a one-byte overrun
// should not cause the program to crash.
const int buflen = 13;

int main(int argc, char *argv[])
{
    char *buf = malloc(buflen);
    int i;
    srand(time(NULL));
    for (i = 0; i < 800000; i++) {
        int msglen = (rand() % 800000 == 0)? buflen + 1 : buflen;
        ovrrun(buf, text, msglen);
    }
    free(buf);
    return 0;
}

在这个程序中, ovrrun 函数是问题的关键。它是 memcpy 的简单封装,没有进行边界检查,因此存在溢出目标缓冲区的风险。主函数中,通过 rand() 函数人为制造了1/800,000的机会使 msglen buflen 大1,从而导致缓冲区溢出。

下面是使用观察点和条件断点调试该程序的对比:
| 调试方式 | 操作命令 | 性能影响 | 原理 |
| — | — | — | — |
| 条件断点 | (gdb) b ovrrun if msglen > buflen | 极慢,程序运行时间大幅增加 | 每次调用 ovrrun 函数时, gdb 都要检查 msglen buflen 的值来决定是否停止程序 |
| 观察点 | (gdb) watch buf[buflen] | 几乎无影响,代码运行速度正常 | 由处理器硬件控制触发,只有在写入 buf[buflen] 位置时才会停止程序 |

使用观察点调试该程序的具体步骤如下:
1. 编译程序: gcc -g -o overrun overrun.c
2. 启动 gdb 并加载程序: gdb overrun
3. 设置观察点: (gdb) watch buf[buflen]
4. 运行程序: (gdb) run
5. 当程序停止时,使用 bt 命令查看调用栈,分析溢出发生的位置。

graph LR
    A[编译程序] --> B(启动gdb并加载程序)
    B --> C(设置观察点: watch buf[buflen])
    C --> D(运行程序: run)
    D --> E{程序是否停止}
    E -->|是| F(使用bt命令查看调用栈)
    E -->|否| D
4. 总结与建议

在调试过程中,我们需要根据不同的情况选择合适的调试方法。以下是一些总结和建议:
- printf 调试
- 优点:简单直接,能快速输出变量的值,帮助了解程序的执行流程。
- 缺点:存在副作用,如意外同步、改变数值结果等。
- 建议:在简单程序或初步调试时可以使用,但对于复杂程序或对精度要求高的程序要谨慎使用。如果可能,使用 -ffloat-store 选项避免浮点数存储问题。
- gdb 调试
- 断点
- 普通断点:适用于已知问题发生的大致位置,如函数入口、关键代码行等。
- 条件断点:当问题的发生与特定条件相关时使用,能更精准地定位问题。设置时要注意变量的作用域。
- 观察点
- 适用于查找内存损坏问题,尤其是难以复现的内存溢出问题。
- 对于支持硬件观察点的架构,性能影响小;对于不支持的架构,性能会大幅下降。
- 建议:熟练掌握 gdb 的各种命令,如 run step next bt 等。利用 Tab 补全功能减少输入错误和工作量。

以下是一个调试方法选择的决策树:

graph TD
    A{问题类型} --> B{简单问题初步排查}
    A --> C{复杂问题精准定位}
    B -->|是| D(使用printf调试)
    C --> E{已知大致位置}
    C --> F{内存损坏问题}
    E -->|是| G(使用普通断点)
    E -->|否| H(使用条件断点)
    F --> I{支持硬件观察点}
    I -->|是| J(使用观察点)
    I -->|否| K(谨慎使用观察点或结合其他方法)

通过合理运用这些调试技巧和工具,开发者可以更高效地解决程序中的问题,提高软件开发的质量和效率。在实际开发中,不断积累调试经验,根据具体情况灵活选择调试方法,才能更好地应对各种复杂的调试场景。

下载前可以先看下教程 https://pan.quark.cn/s/16a53f4bd595 小天才电话手表刷机教程 — 基础篇 我们将为您简单的介绍小天才电话手表新机型的简单刷机以及玩法,如adb工具的使用,magisk的刷入等等。 我们会确保您看完此教程后能够对Android系统有一个最基本的认识,以及能够成功通过magisk root您的手表,并安装您需要的第三方软件。 ADB Android Debug Bridge,简称,在android developer的adb文档中是这么描述它的: 是一种多功能命令行工具,可让您设备进行通信。 该命令有助于各种设备操作,例如安装和调试应用程序。 提供对 Unix shell 的访问,您可以使用它在设备上运行各种命令。 它是一个客户端-服务器程序。 这听起来有些难以理解,因为您也没有必要去理解它,如果您对本文中的任何关键名词产生疑惑或兴趣,您都可以在搜索引擎中去搜索它,当然,我们会对其进行简单的解释:是一款在命令行中运行的,用于对Android设备进行调试的工具,并拥有比一般用户以及程序更高的权限,所以,我们可以使用它对Android设备进行最基本的调试操作。 而在小天才电话手表上启用它,您只需要这么做: - 打开拨号盘; - 输入; - 点按打开adb调试选项。 其次是电脑上的Android SDK Platform-Tools的安装,此工具是 Android SDK 的组件。 它包括 Android 平台交互的工具,主要由和构成,如果您接触过Android开发,必然会使用到它,因为它包含在Android Studio等IDE中,当然,您可以独立下载,在下方选择对应的版本即可: - Download SDK Platform...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值