在汽车嵌入式开发领域,软件复杂度不断提升,从传统的发动机控制到现代的智能座舱和自动驾驶系统,软件代码量呈指数级增长。这种复杂性带来了更高的调试难度,而 GDB(GNU Debugger)作为一款功能强大的调试工具,成为了汽车嵌入式开发者的必备技能。
备注:文章思路来源和工程师交流过程中,大家提到前期有没有免费快捷的调试工具,想起来前几年的笔记分享一下,一起交流学习。
掌握 GDB 对于汽车嵌入式开发者的重要性体现在以下几个方面:
本指南将从 GDB 基础入手,逐步深入到汽车嵌入式开发的高级应用,特别关注 iSYSTEM 调试器与 GDB 的结合使用,为汽车嵌入式开发者提供全面的 GDB 使用指南。
GDB(GNU Debugger)是一个功能强大的开源调试工具,由 GNU 项目开发和维护。它支持多种编程语言,包括 C、C++、Fortran 等,是软件开发中最常用的调试工具之一。
GDB 的主要功能包括:
GDB 是一个命令行工具,通过输入命令来控制调试过程。虽然它没有图形界面,但提供了丰富的命令和灵活的调试能力,是专业开发者的首选调试工具。
要使用 GDB 调试程序,首先需要在编译时添加调试信息。调试信息包含了源代码与可执行代码之间的映射关系,使 GDB 能够将机器代码与源代码对应起来。
在使用 GCC 编译时,添加 -g 选项可以生成调试信息:
# 编译 C 程序
gcc -g program.c -o program
# 编译 C++ 程序
g++ -g program.cpp -o program
# 优化级别设置
# 注意:优化级别过高可能会影响调试效果,建议使用 -O0 或 -O1
gcc -g -O0 program.c -o program
对于汽车嵌入式开发中常用的交叉编译环境,同样需要添加 -g 选项:
# ARM Cortex-M 平台
arm-none-eabi-gcc -g -mcpu=cortex-m4 -mthumb program.c -o program
# Tricore 平台
tricore-gcc -g program.c -o program
# 基本启动方式
gdb program
# 交叉编译环境
tricore-gdb program
arm-none-eabi-gdb program
gdb 命令后加载可执行文件# 启动 GDB
gdb
# 在 GDB 提示符下加载可执行文件
(gdb) file program
# 查看进程 ID
ps aux | grep program
# 附加到进程
gdb -p <pid>
# 或在 GDB 中附加
(gdb) attach <pid>
|
|
|
|
|---|---|---|
run |
r |
|
break |
b |
|
continue |
c |
|
next |
n |
|
step |
s |
|
print |
p |
|
backtrace |
bt |
|
info breakpoints |
info b |
|
delete |
d |
|
watch |
|
|
quit |
q |
|
list |
l |
|
whatis |
|
|
set variable |
set var |
|
until |
u |
|
disable |
|
|
enable |
|
|
tbreak |
|
|
info locals |
|
|
info args |
|
|
条件断点允许在特定条件满足时暂停程序执行,这在调试循环或需要特定条件触发的问题时非常有用。
# 设置条件断点
(gdb) break line_number if condition
# 示例:当 i 等于 100 时暂停
(gdb) break 42 if i == 100
# 示例:当指针不为空时暂停
(gdb) break process_data if ptr != NULL
观察点用于监控变量或内存位置的变化,当变量值或内存内容发生变化时,程序会暂停执行。
# 设置观察点
(gdb) watch variable
# 示例:监控全局变量 counter 的变化
(gdb) watch counter
# 监控内存位置
(gdb) watch *0x12345678
# 查看观察点
(gdb) info watchpoints
临时断点在触发一次后会自动删除,适用于只需要检查一次的场景。
# 设置临时断点
(gdb) tbreak line_number
(gdb) tbreak function_name
GDB 提供了 x 命令用于检查内存内容,可以以不同格式查看内存数据。
# 查看内存内容
# 格式:x/[n][f][u] address
# n: 显示的单元数
# f: 显示格式(x 十六进制, d 十进制, u 无符号十进制, o 八进制, t 二进制, a 地址, i 指令, c 字符, s 字符串)
# u: 单元大小(b 字节, h 半字, w 字, g 双字)
# 示例:以十六进制查看从 0x1000 开始的 10 个 32 位字
(gdb) x/10xw 0x1000
# 示例:查看字符串
(gdb) x/s 0x2000
# 示例:查看指令
(gdb) x/5i $pc
GDB 可以查看和修改 CPU 寄存器的值,这对于底层调试非常重要。
# 查看所有寄存器
(gdb) info registers
# 查看特定寄存器
(gdb) print $pc # 程序计数器
(gdb) print $sp # 栈指针
(gdb) print $fp # 帧指针
(gdb) print $r0 # ARM 寄存器
(gdb) print $eax # x86 寄存器
# 修改寄存器值
(gdb) set $pc = 0x1000
(gdb) set $r0 = 42
GDB 支持多线程调试,可以查看和切换线程,设置线程特定的断点。
# 查看线程信息
(gdb) info threads
# 切换到特定线程
(gdb) thread <thread_id>
# 设置线程特定断点
(gdb) break function_name thread <thread_id>
# 查看当前线程的调用栈
(gdb) thread apply all bt
GDB 也支持多进程调试,可以跟踪子进程的执行。
# 跟踪子进程
(gdb) set follow-fork-mode child
# 同时调试父进程和子进程
(gdb) set detach-on-fork off
# 切换到特定进程
(gdb) inferior <inferior_id>
# 查看进程信息
(gdb) info inferiors
调用栈分析是调试中的重要环节,它显示了函数的调用关系,帮助开发者理解程序的执行流程。
# 查看调用栈
(gdb) backtrace
(gdb) bt
# 查看特定数量的栈帧
(gdb) bt 5
# 切换到特定栈帧
(gdb) frame <frame_number>
# 查看栈帧信息
(gdb) info frame
# 查看局部变量
(gdb) info locals
# 查看函数参数
(gdb) info args
在汽车嵌入式开发中,除了 GDB 之外,还有其他多种调试工具可供选择。了解这些工具的特点和适用场景,有助于开发者选择最合适的调试工具。
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
远程调试是嵌入式开发的核心技术之一,它允许开发者在主机上通过调试器控制目标设备上的程序执行。
对于运行 Linux 的嵌入式设备,可以使用 gdbserver 进行远程调试。
# 在目标设备上启动 gdbserver
target$ gdbserver :2345 program
# 在主机上启动 GDB 并连接
host$ gdb program
host$ (gdb) target remote <target_ip>:2345
# 断开远程连接
host$ (gdb) detach
# 重新连接
host$ (gdb) target remote <target_ip>:2345
对于没有操作系统的裸机系统,可以使用 JTAG 或 SWD 接口进行调试。
# 使用 OpenOCD 作为调试服务器
openocd -f board/stm32f4discovery.cfg
# 在主机上启动 GDB 并连接
host$ arm-none-eabi-gdb program
host$ (gdb) target remote :3333
host$ (gdb) load # 加载程序到目标设备
# 重置目标设备
host$ (gdb) monitor reset
# 继续执行
host$ (gdb) continue
# 设置远程调试超时
host$ (gdb) set remotetimeout 60
# 查看远程目标信息
host$ (gdb) info target
# 在远程目标上执行命令
host$ (gdb) monitor <command>
# 加载符号文件
host$ (gdb) symbol-file <symbol_file>
# 加载共享库符号
host$ (gdb) sharedlibrary
Core Dump 文件包含了程序崩溃时的内存状态,通过分析 Core Dump 文件,可以确定程序崩溃的原因。
# 启用 Core Dump(在 Linux 系统中)
$ ulimit -c unlimited
# 调试 Core Dump 文件
gdb program core
# 查看崩溃位置
(gdb) bt
# 查看崩溃时的变量值
(gdb) print variable
# 查看崩溃时的寄存器状态
(gdb) info registers
# 查看崩溃时的内存状态
(gdb) x/16xw $sp
# 查看共享库信息
(gdb) info sharedlibrary
# 查看线程信息
(gdb) info threads
# 切换到崩溃的线程
(gdb) thread apply all bt
.gdbinit 文件是 GDB 的初始化文件,在启动 GDB 时会自动执行其中的命令。可以在其中添加常用的命令和设置,提高调试效率。
# .gdbinit 文件示例
set pagination off
set print pretty on
break main
define hook-run
echo Starting program...
end
GDB 支持 Python 脚本,可以通过 Python 扩展 GDB 的功能,实现更复杂的调试任务。
# Python 扩展示例
import gdb
class HelloWorld(gdb.Command):
def __init__(self):
super(HelloWorld, self).__init__("hello", gdb.COMMAND_USER)
def invoke(self, arg, from_tty):
print("Hello, GDB!")
HelloWorld()
将上述代码保存为 hello.py,然后在 GDB 中加载:
(gdb) source hello.py
(gdb) hello
Hello, GDB!
GDB 的 TUI(Text User Interface)模式提供了类似 IDE 的界面,同时显示源代码、汇编代码和寄存器等信息。
# 启动 GDB 时启用 TUI 模式
gdb -tui program
# 在 GDB 中切换 TUI 模式
(gdb) layout src # 显示源代码
(gdb) layout asm # 显示汇编代码
(gdb) layout regs # 显示寄存器
(gdb) layout split # 同时显示源代码和汇编代码
(gdb) layout next # 切换到下一个布局
(gdb) layout prev # 切换到上一个布局
(gdb) tui disable # 禁用 TUI 模式
# 刷新 TUI 界面
(gdb) tui refresh
# 调整 TUI 窗口大小
(gdb) tui reg float # 显示浮点寄存器
(gdb) tui reg system # 显示系统寄存器
# 在 TUI 模式下执行命令
(gdb) tui enable
(gdb) info breakpoints
示例程序:
// main.c
#include <stdio.h>
int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}
int main() {
int n = 5;
int result = factorial(n);
printf("Factorial of %d is %d
", n, result);
return 0;
}
编译:
gcc -g main.c -o main
调试交互过程:
$ gdb main
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
Copyright (C) 2020 Free Software Foundation, Inc.
...
Reading symbols from main...done.
(gdb) break main
Breakpoint 1 at 0x1149: file main.c, line 13.
(gdb) run
Starting program: /home/user/main
Breakpoint 1, main () at main.c:13
13 int n = 5;
(gdb) print n
$1 = 0
(gdb) next
14 int result = factorial(n);
(gdb) print n
$2 = 5
(gdb) step
factorial (n=5) at main.c:4
4 if (n <= 1) {
(gdb) print n
$3 = 5
(gdb) continue
Continuing.
Factorial of 5 is 120
[Inferior 1 (process 12345) exited normally]
(gdb) quit
目标设备操作:
# 在目标设备上启动 gdbserver
target$ gdbserver :2345 main
Process main created; pid = 6789
Listening on port 2345
主机操作:
$ gdb main
(gdb) target remote 192.168.1.100:2345
Remote debugging using 192.168.1.100:2345
0x0000aaaad0010310 in _start () from /lib/ld-linux-aarch64.so.1
(gdb) break main
Breakpoint 1 at 0xaaaad00105a8: file main.c, line 13.
(gdb) continue
Continuing.
Breakpoint 1, main () at main.c:13
13 int n = 5;
(gdb) print n
$1 = 0
(gdb) next
14 int result = factorial(n);
(gdb) print n
$2 = 5
(gdb) step
factorial (n=5) at main.c:4
4 if (n <= 1) {
(gdb) print n
$3 = 5
(gdb) continue
Continuing.
Factorial of 5 is 120
[Inferior 1 (process 6789) exited normally]
(gdb) quit
示例程序:
// memory_error.c
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = malloc(5 * sizeof(int));
if (ptr == NULL) {
printf("Memory allocation failed
");
return 1;
}
// 写入越界
for (int i = 0; i < 10; i++) {
ptr[i] = i;
}
free(ptr);
return 0;
}
调试交互过程:
$ gdb memory_error
(gdb) run
Starting program: /home/user/memory_error
Program received signal SIGSEGV, Segmentation fault.
0x0000555555555175 in main () at memory_error.c:12
12 ptr[i] = i;
(gdb) print i
$1 = 5
(gdb) print ptr
$2 = (int *) 0x5555555592a0
(gdb) x/10xw ptr
0x5555555592a0: 0x00000000 0x00000001 0x00000002 0x00000003
0x5555555592b0: 0x00000004 0x00000000 0x00000000 0x00000000
0x5555555592c0: 0x00000000 0x00000000
(gdb) backtrace
#0 0x0000555555555175 in main () at memory_error.c:12
(gdb) quit
示例程序:
// thread_demo.c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int counter = 0;
pthread_mutex_t mutex;
void *increment(void *arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&mutex);
counter++;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&mutex, NULL);
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final counter value: %d
", counter);
pthread_mutex_destroy(&mutex);
return 0;
}
调试交互过程:
$ gdb thread_demo
(gdb) break increment
Breakpoint 1 at 0x1189: file thread_demo.c, line 10.
(gdb) run
Starting program: /home/user/thread_demo
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff77f3700 (LWP 12346)]
[New Thread 0x7ffff6ff2700 (LWP 12347)]
Thread 2 "thread_demo" hit Breakpoint 1, increment (arg=0x0) at thread_demo.c:10
10 for (int i = 0; i < 100000; i++) {
(gdb) info threads
Id Target Id Frame
* 2 Thread 0x7ffff77f3700 (LWP 12346) "thread_demo" increment (arg=0x0) at thread_demo.c:10
3 Thread 0x7ffff6ff2700 (LWP 12347) "thread_demo" 0x00007ffff7f8a94d in __lll_lock_wait () from /lib/x86_64-linux-gnu/libpthread.so.0
1 Thread 0x7ffff7f91740 (LWP 12342) "thread_demo" pthread_join (threadid=140737345775360, thread_return=0x0) at pthread_join.c:83
(gdb) print counter
$1 = 12345
(gdb) thread 3
[Switching to thread 3 (Thread 0x7ffff6ff2700 (LWP 12347))]
#0 0x00007ffff7f8a94d in __lll_lock_wait () from /lib/x86_64-linux-gnu/libpthread.so.0
(gdb) backtrace
#0 0x00007ffff7f8a94d in __lll_lock_wait () from /lib/x86_64-linux-gnu/libpthread.so.0
#1 0x00007ffff7f88573 in pthread_mutex_lock () from /lib/x86_64-linux-gnu/libpthread.so.0
#2 0x00005555555551b1 in increment (arg=0x0) at thread_demo.c:11
(gdb) continue
Continuing.
[Thread 0x7ffff77f3700 (LWP 12346) exited]
[Thread 0x7ffff6ff2700 (LWP 12347) exited]
Final counter value: 200000
[Inferior 1 (process 12342) exited normally]
(gdb) quit
编译:
arm-none-eabi-gcc -g -O0 -mcpu=cortex-m4 -mthumb -T stm32f407vg.ld main.c -o main.elf
调试交互过程:
# 启动 OpenOCD
$ openocd -f interface/stlink-v2.cfg -f target/stm32f4x.cfg
# 启动 GDB
$ arm-none-eabi-gdb main.elf
(gdb) target remote :3333
Remote debugging using :3333
0x08000200 in Reset_Handler ()
(gdb) load
Loading section .text, size 0x1000 lma 0x8000000
Loading section .data, size 0x100 lma 0x8001000
Start address 0x8000200, load size 4352
Transfer rate: 17 KB/sec, 4352 bytes/write.
(gdb) break main
Breakpoint 1 at 0x8000500: file main.c, line 10.
(gdb) monitor reset
Resetting target
(gdb) continue
Continuing.
Breakpoint 1, main () at main.c:10
10 int n = 5;
(gdb) print n
$1 = 0
(gdb) next
11 int result = factorial(n);
(gdb) print n
$2 = 5
(gdb) step
factorial (n=5) at main.c:4
4 if (n <= 1) {
(gdb) print n
$3 = 5
(gdb) continue
Continuing.
Program received signal SIGINT, Interrupt.
0x080004f0 in factorial (n=1) at main.c:6
6 return 1;
(gdb) print n
$4 = 1
(gdb) continue
Continuing.
[Inferior 1 (Remote target) exited normally]
(gdb) quit
WinIDEA 配置:
调试交互过程:
$ arm-none-eabi-gdb main.elf
(gdb) target remote localhost:2331
Remote debugging using localhost:2331
0x08000200 in Reset_Handler ()
(gdb) load
Loading section .text, size 0x1000 lma 0x8000000
Loading section .data, size 0x100 lma 0x8001000
Start address 0x8000200, load size 4352
Transfer rate: 17 KB/sec, 4352 bytes/write.
(gdb) break main
Breakpoint 1 at 0x8000500: file main.c, line 10.
(gdb) monitor reset
Resetting target
(gdb) continue
Continuing.
Breakpoint 1, main () at main.c:10
10 int n = 5;
(gdb) print n
$1 = 0
(gdb) next
11 int result = factorial(n);
(gdb) print n
$2 = 5
(gdb) step
factorial (n=5) at main.c:4
4 if (n <= 1) {
(gdb) print n
$3 = 5
(gdb) continue
Continuing.
[Inferior 1 (Remote target) exited normally]
(gdb) quit
(gdb) break main
(gdb) commands
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
> print variable
> continue
> end
(gdb) print/x variable # 十六进制
(gdb) print/d variable # 十进制
(gdb) print/t variable # 二进制
print 命令查看数组和结构体的完整内容
(gdb) print array
(gdb) print *struct_ptr
whatis 和 ptype 命令查看变量类型
(gdb) whatis variable
(gdb) ptype variable
set 命令填充内存区域
(gdb) set {int}0x1000 = 0xdeadbeef
compare 命令比较内存区域
(gdb) compare memory 0x1000 0x1010 0x2000
find 命令在内存中搜索特定值
(gdb) find 0x1000, 0x2000, 0xdeadbeef
thread apply 命令对特定线程执行操作
(gdb) thread apply 1-3 bt
(gdb) thread name 1 "main thread"
(gdb) thread 2
原因:
解决方案:
-O0 或 -O1)原因:
解决方案:
volatile 关键字防止变量被优化原因:
解决方案:
原因:
-g 选项解决方案:
-g 选项sharedlibrary 命令加载共享库符号原因:
解决方案:
在汽车嵌入式开发中,不同架构的单片机对 GDB 的支持程度有所不同,这主要取决于架构的普及程度、工具链的成熟度以及行业的使用习惯。
背景:ARM 架构在汽车嵌入式领域占据了主导地位,特别是 Cortex-M 系列单片机被广泛应用于车身控制、传感器节点等场景。
GDB 支持情况:
适用场景:适用于从简单的 8 位/16 位 MCU 到复杂的 32 位处理器,覆盖了汽车电子的大多数应用场景。
背景:英飞凌 TC3xx 系列基于 TriCore 架构,是汽车电子领域的高端控制器,广泛应用于发动机控制、变速箱控制等安全关键系统。
GDB 支持情况:
适用场景:主要用于需要高性能和功能安全认证的汽车控制系统。
GDB 支持多种架构的单片机,主要包括:
ARM 内核的国产单片机:
RISC-V 架构的国产单片机:
GDB 支持情况:
优势:
-g 选项生成调试信息-O0 或 -O1)步骤 1:安装必要软件
# Windows
# 下载并安装 OpenOCD、arm-none-eabi-gcc、GDB
# Linux
sudo apt-get install openocd gcc-arm-none-eabi gdb-multiarch
# macOS
brew install openocd gcc-arm-none-eabi gdb
步骤 2:配置 OpenOCD
# 创建或使用现有的 OpenOCD 配置文件
# 例如:stm32f4discovery.cfg
步骤 3:启动 OpenOCD
openocd -f interface/jlink.cfg -f target/stm32f4x.cfg
步骤 4:编译程序
arm-none-eabi-gcc -g -O0 -mcpu=cortex-m4 -mthumb -T linker.ld main.c -o app.elf
步骤 5:启动 GDB 并连接
arm-none-eabi-gdb app.elf
(gdb) target remote :3333
(gdb) load
(gdb) break main
(gdb) continue
步骤 1:安装 J-Link 软件包
步骤 2:启动 J-Link GDB Server
JLinkGDBServer -device STM32F407VG -if SWD -speed 4000
步骤 3:编译程序
arm-none-eabi-gcc -g -O0 -mcpu=cortex-m4 -mthumb -T linker.ld main.c -o app.elf
步骤 4:启动 GDB 并连接
arm-none-eabi-gdb app.elf
(gdb) target remote :2331
(gdb) load
(gdb) break main
(gdb) continue
架构:ARM64(如 NXP i.MX8、R-Car、NVIDIA Orin)
用法:标准 Linux GDB + gdbserver
适用性:★★★★★
调试流程:
架构:ARM Cortex-M(如 STM32、NXP S32K)
用法:OpenOCD + arm-none-eabi-gdb(JTAG/SWD)
适用性:★★★★☆
多任务调试:借助 Python 脚本查看 RTOS 任务列表
架构:ARM64/x86
用法:ntoarmv7-gdb / qnx-gdb + gdbserver
适用性:★★★★☆
简介:iSYSTEM 是汽车行业常用的硬件调试工具厂商,其 WinIDEA 集成开发环境支持通过 GDB 服务器接口进行调试。
使用方法:
# 根据目标架构选择合适的 GDB
# ARM 架构
host$ arm-none-eabi-gdb application.elf
# Tricore 架构
host$ tricore-gdb application.elf
# RH850 架构
host$ rh850-gdb application.elf
# 连接到 WinIDEA GDB 服务器
(gdb) target remote <winidea_ip>:2331
优势:
-g 选项生成调试信息watch 命令监控可疑内存位置x 命令查看内存内容,寻找异常值info threads 查看线程状态bt 命令查看每个线程的调用关系time 命令测量函数执行时间问题描述:ECU 在处理凸轮轴位置传感器信号时出现偶发性错误,导致发动机失火。
调试步骤:
(gdb) break process_camshaft_signal
continue 命令运行程序,等待断点触发(gdb) print sensor_value
(gdb) print processed_value
(gdb) x/10xw &sensor_buffer
问题描述:基于 Linux 的智能座舱系统启动时间超过 10 秒,不符合用户体验要求。
调试步骤:
target$ gdbserver :2345 system_startup
host$ aarch64-linux-gnu-gdb system_startup
host$ (gdb) target remote <target_ip>:2345
(gdb) break init_display
(gdb) break load_applications
(gdb) break init_communication
time 命令测量每个函数的执行时间
(gdb) time init_display
GDB 作为一款功能强大的调试工具,在汽车嵌入式开发中发挥着重要作用。通过本指南的学习,开发者应该掌握以下内容:
随着汽车电子系统的不断发展,调试工具也在不断进化。未来 GDB 在汽车嵌入式开发中的应用将呈现以下趋势:
掌握 GDB 调试技巧是汽车嵌入式开发者的必备技能。通过不断学习和实践,开发者可以提高调试效率,快速解决软件问题,确保汽车电子系统的可靠性和安全性。
在未来的汽车嵌入式开发中,GDB 将继续发挥重要作用,帮助开发者应对日益复杂的软件挑战,推动汽车电子技术的不断创新和进步。
对于需要满足功能安全要求(如 ISO 26262)的汽车嵌入式项目,除了掌握 GDB 调试技巧外,还建议考虑以下专业工具:
优势:
适用场景:
优势:
适用场景:
在实际项目中,建议:
通过合理选择和使用调试工具,可以显著提高汽车嵌入式系统的开发效率和质量,确保系统的安全性和可靠性。
欢迎大家一起交流学习,如遇到问题,欢迎留言讨论。
support@softor.com.cn
tianpengbo@softor.com.cn
作者:tianpengbo / 田朋博。大家如果在项目中遇到相关技术问题,欢迎联系我交流。
support@softor.com.cn
tianpengbo@softor.com.cn
作者:tianpengbo / 田朋博。大家如果在项目中遇到相关技术问题,欢迎联系我交流。
support@softor.com.cn
tianpengbo@softor.com.cn