找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

662

积分

0

好友

80

主题
发表于 前天 03:47 | 查看: 6| 回复: 0

当线上服务突然崩溃,你是否还在用到处打 printf 的方式排查问题?在日志中翻来覆去,却始终找不到崩溃的根本原因。这种原始的调试方式不仅效率低下,更重要的是,它让你错失了程序崩溃时的完整现场信息。

GDB(GNU Debugger)作为Linux平台最强大的调试工具,远不止是设置断点加单步执行那么简单。它就像是程序的CT扫描仪,能在程序崩溃瞬间保存完整的内存状态、调用栈、寄存器值等关键信息,让你在事后也能精准还原崩溃现场。

本文将深入探讨GDB的高阶调试技巧,从Core Dump分析、死锁定位、内存踩踏诊断到Sanitizer工具的应用,带你系统掌握现代C/C++调试的核心技术。这些技能不仅能提升你的调试效率,更能让你在复杂的生产环境问题面前游刃有余。

一、Core Dump分析实战:程序崩溃的“事后取证”

Core Dump文件的生成与配置

Core Dump是程序崩溃时系统生成的内存镜像文件,记录了进程崩溃时的完整状态。要使用Core Dump分析,首先需要正确配置系统。

启用Core Dump生成

# 临时启用(当前会话有效)
ulimit -c unlimited

# 永久生效(修改系统配置)
echo "* soft core unlimited" | sudo tee -a /etc/security/limits.conf

设置Core文件保存路径

# 设置core文件命名格式(包含程序名、进程ID、时间戳)
echo "/tmp/core-%e-%p-%t" | sudo tee /proc/sys/kernel/core_pattern

参数说明:

  • %e:程序名
  • %p:进程ID
  • %t:崩溃时间戳

使用GDB分析Core Dump

基本分析流程

# 启动GDB加载core文件
gdb ./your_program /tmp/core-your_program-12345-1620000000

# 查看崩溃时的调用栈(核心命令!)
(gdb) bt

典型崩溃场景分析

场景1:空指针解引用

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

void process_data(char* ptr){
printf("Processing data at %p\n", ptr);
    *ptr = 'X';  // 崩溃点:空指针解引用
}

int main(){
    process_data(NULL);  // 传入空指针
return 0;
}

GDB分析步骤:

(gdb) bt
#0  0x00000000004005f6 in process_data (ptr=0x0) at test.c:7
#1  0x0000000000400612 in main () at test.c:12

(gdb) frame 0
#0  process_data (ptr=0x0) at test.c:7
7       *ptr = 'X';

(gdb) p ptr
$1 = 0x0

(gdb) info locals
ptr = 0x0

诊断结论: ptr指针为NULL,导致在test.c:7行发生空指针解引用崩溃。

场景2:数组越界访问

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

void process_buffer(){
int buffer[10];
for (int i = 0; i <= 10; i++) {
        buffer[i] = i * 10;  // i=10时越界
    }
}

int main(){
    process_buffer();
return 0;
}

GDB分析步骤:

(gdb) bt
#0  0x00000000004005e0 in process_buffer () at test.c:8
#1  0x0000000000400608 in main () at test.c:13

(gdb) frame 0
#0  process_buffer () at test.c:8
8           buffer[i] = i * 10;

(gdb) p i
$2 = 10

(gdb) p buffer
$3 = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90}

(gdb) x/10xw $rbp-40  # 查看栈内存
0x7fffffffd9d0: 0x00000000  0x0000000a  0x00000014  0x0000001e

场景3:堆内存越界

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

void heap_overflow(){
char* buffer = (char*)malloc(16);
if (buffer) {
strcpy(buffer, "This string is too long for the buffer!");
free(buffer);
    }
}

int main(){
    heap_overflow();
return 0;
}

GDB分析步骤:

(gdb) bt
#0  0x00007ffff7a890b5 in __GI___libc_free (mem=0x602010) at malloc.c:2929
#1  0x0000000000400643 in heap_overflow () at test.c:9
#2  0x000000000040065a in main () at test.c:14

(gdb) frame 1
#1  heap_overflow () at test.c:9
9           free(buffer);

(gdb) p buffer
$1 = 0x602010 “This string is too long for the buffer!”

(gdb) p *(char(*)[16])0x602010
$2 = “This string i”

诊断结论: 分配了16字节内存,但写入了超过16字节的数据,导致堆溢出。

高级技巧:生产环境Core Dump分析

问题: 生产环境通常使用strip过的可执行文件,没有调试符号信息。

解决方案:使用独立符号文件

# 启动GDB
gdb

# 设置debug文件目录
(gdb) set debug-file-directory /usr/lib/debug/:/usr/lib/debug

# 加载无符号的可执行文件
(gdb) file /path/to/binary_with_no_symbols

# 加载core dump
(gdb) core-file /log/coredump/core-xxx-146007-1766839168

# 现在可以查看完整的调用栈了!
(gdb) bt full
(gdb) info threads
(gdb) thread apply all bt

二、死锁定位专题:多线程调试的艺术

死锁产生的原理与常见特征

死锁定义: 两个或多个线程因竞争资源而相互等待,导致所有线程都无法继续执行的状态。

典型死锁场景:

#include<pthread.h>
#include<stdio.h>

pthread_mutex_t mutexA = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutexB = PTHREAD_MUTEX_INITIALIZER;

void* thread1_func(void* arg){
    pthread_mutex_lock(&mutexA);
printf(“Thread 1: Acquired mutexA\n“);
    sleep(1);  // 增加死锁发生概率

printf(“Thread 1: Trying to acquire mutexB\n“);
    pthread_mutex_lock(&mutexB);  // 等待mutexB

printf(“Thread 1: Acquired both mutexes\n“);
    pthread_mutex_unlock(&mutexB);
    pthread_mutex_unlock(&mutexA);
return NULL;
}

void* thread2_func(void* arg){
    pthread_mutex_lock(&mutexB);
printf(“Thread 2: Acquired mutexB\n“);
    sleep(1);  // 增加死锁发生概率

printf(“Thread 2: Trying to acquire mutexA\n“);
    pthread_mutex_lock(&mutexA);  // 等待mutexA

printf(“Thread 2: Acquired both mutexes\n“);
    pthread_mutex_unlock(&mutexA);
    pthread_mutex_unlock(&mutexB);
return NULL;
}

int main(){
pthread_t thread1, thread2;

    pthread_create(&thread1, NULL, thread1_func, NULL);
    pthread_create(&thread2, NULL, thread2_func, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

return 0;
}

死锁特征:

  • 程序无响应,但CPU使用率正常
  • 多个线程阻塞在 pthread_mutex_lock
  • 锁的获取顺序不一致

使用GDB定位死锁的具体步骤

第1步:附加到运行中的进程

# 找到目标进程ID
ps -ef | grep your_program

# 使用GDB附加
sudo gdb -p <PID>

第2步:查看所有线程状态

(gdb) info threads
  Id   Target Id         Frame
* 1    Thread 0x7ffff7fdb700 (LWP 12345) __lll_lock_wait () at lowlevellock.c:52
  2    Thread 0x7ffff7ffb700 (LWP 12346) __lll_lock_wait () at lowlevellock.c:52

观察要点:

  • 星号 * 标记当前选中的线程
  • 多个线程阻塞在 __lll_lock_wait 上,可能存在死锁

第3步:分析每个线程的调用栈

(gdb) thread apply all bt

Thread 1 (LWP 12345):
#0  __lll_lock_wait () at lowlevellock.c:52
#1  0x00007ffff7bd1e24 in pthread_mutex_lock (mutex=0x55555555a2a0) at pthread_mutex_lock.c:115
#2  0x00005555555551a9 in thread1_func (arg=0x0) at deadlock.c:22

Thread 2 (LWP 12346):
#0  __lll_lock_wait () at lowlevellock.c:52
#1  0x00007ffff7bd1e24 in pthread_mutex_lock (mutex=0x55555555a2c0) at pthread_mutex_lock.c:115
#2  0x00005555555551e2 in thread2_func (arg=0x0) at deadlock.c:34

第4步:切换到特定线程分析锁状态

(gdb) thread 1
[Switching to thread 1 (Thread 0x7ffff7fdb700)]

(gdb) frame 2
#2  0x00005555555551a9 in thread1_func (arg=0x0) at deadlock.c:22
22          pthread_mutex_lock(&mutexB);

(gdb) p mutexA
$1 = {__data = {__lock = 1, __count = 0, __owner = 12345, __nusers = 1, …}}

(gdb) p mutexB
$2 = {__data = {__lock = 2, __count = 0, __owner = 12346, __nusers = 2, …}}

关键信息解读:

  • __lock = 1:线程1持有mutexA
  • __lock = 2:mutexB被线程2持有,线程1在等待
  • __owner:显示当前持有锁的线程ID

第5步:构建锁依赖图

# 线程1分析
(gdb) thread 1
(gdb) p *((pthread_mutex_t *)0x55555555a2a0)  # mutexA
(gdb) p *((pthread_mutex_t *)0x55555555a2c0)  # mutexB

# 线程2分析
(gdb) thread 2
(gdb) p *((pthread_mutex_t *)0x55555555a2a0)  # mutexA
(gdb) p *((pthread_mutex_t *)0x55555555a2c0)  # mutexB

死锁分析表:

线程 持有锁 等待锁 位置
1 mutexA mutexB deadlock.c:22
2 mutexB mutexA deadlock.c:34

结论: 线程1持有mutexA等待mutexB,线程2持有mutexB等待mutexA,形成典型的死锁环。

避免死锁的实用编程建议

1. 统一锁获取顺序

// 错误:锁顺序不一致
void bad_thread1(){
    pthread_mutex_lock(&mutexA);
    pthread_mutex_lock(&mutexB);
// …
    pthread_mutex_unlock(&mutexB);
    pthread_mutex_unlock(&mutexA);
}

void bad_thread2(){
    pthread_mutex_lock(&mutexB);
    pthread_mutex_lock(&mutexA);
// …
    pthread_mutex_unlock(&mutexA);
    pthread_mutex_unlock(&mutexB);
}

// 正确:统一锁顺序
void good_thread1(){
    pthread_mutex_lock(&mutexA);
    pthread_mutex_lock(&mutexB);
// …
    pthread_mutex_unlock(&mutexB);
    pthread_mutex_unlock(&mutexA);
}

void good_thread2(){
    pthread_mutex_lock(&mutexA);  // 先获取mutexA
    pthread_mutex_lock(&mutexB);
// …
    pthread_mutex_unlock(&mutexB);
    pthread_mutex_unlock(&mutexA);
}

2. 使用trylock配合超时

#include<pthread.h>
#include<time.h>
#include<errno.h>

int lock_with_timeout(pthread_mutex_t* mutex, int timeout_ms){
struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
    ts.tv_sec += timeout_ms / 1000;
    ts.tv_nsec += (timeout_ms % 1000) * 1000000;
if (ts.tv_nsec >= 1000000000) {
        ts.tv_sec++;
        ts.tv_nsec -= 1000000000;
    }

return pthread_mutex_timedlock(mutex, &ts);
}

void safe_thread(){
if (lock_with_timeout(&mutexA, 1000) == 0) {
if (lock_with_timeout(&mutexB, 1000) == 0) {
// 获取两个锁成功
            pthread_mutex_unlock(&mutexB);
            pthread_mutex_unlock(&mutexA);
        } else {
// 获取mutexB失败,释放mutexA
            pthread_mutex_unlock(&mutexA);
        }
    }
}

3. 使用C++ RAII封装

#include<mutex>
#include<memory>

class LockGuard {
private:
std::mutex& mutex_;
public:
    LockGuard(std::mutex& m) : mutex_(m) {
        mutex_.lock();
    }
    ~LockGuard() {
        mutex_.unlock();
    }
// 禁止拷贝
    LockGuard(const LockGuard&) = delete;
    LockGuard& operator=(const LockGuard&) = delete;
};

void safe_function(){
static std::mutex mutexA, mutexB;

// 使用std::lock避免死锁
std::lock(mutexA, mutexB);

std::lock_guard<std::mutex> lockA(mutexA, std::adopt_lock);
std::lock_guard<std::mutex> lockB(mutexB, std::adopt_lock);

// 临界区代码
// 异常安全:自动释放锁
}

三、内存踩踏问题诊断:隐形的杀手

内存踩踏的表现形式与危害

内存踩踏是指程序错误地修改了不属于它的内存区域,导致数据损坏、程序崩溃或逻辑错误。

常见形式:

  • 数组越界访问
  • 使用已释放的内存(Use-After-Free)
  • 栈溢出
  • 堆溢出
  • 野指针操作

危害:

  • 程序崩溃(Segmentation Fault)
  • 数据损坏(计算结果错误)
  • 安全漏洞(缓冲区溢出攻击)
  • 难以复现的问题(偶发性错误)

使用GDB查看内存踩踏现场

案例:数组越界导致的内存踩踏

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

struct Data {
int id;
char name[16];
double value;
};

void corrupt_memory(){
int array[10];

printf(“Before corruption:\n“);
printf(“array[9] address: %p\n“, &array[9]);
printf(“array[10] address: %p\n“, &array[10]);

// 越界写入
for (int i = 0; i <= 15; i++) {
array[i] = i * 100;
    }

printf(“\nAfter corruption:\n“);
for (int i = 0; i <= 15; i++) {
printf(“array[%d] = %d at %p\n“, i, array[i], &array[i]);
    }
}

int main(){
    corrupt_memory();
return 0;
}

GDB调试步骤

# 编译时包含调试信息
gcc -g -O0 -fno-omit-frame-pointer -o corrupt corrupt.c

# 运行程序观察行为
./corrupt

使用GDB监控关键内存:

(gdb) break corrupt_memory
Breakpoint 1 at 0x400586: file corrupt.c:7.

(gdb) run
Breakpoint 1, corrupt_memory () at corrupt.c:8

(gdb) p &array
$1 = (int (*)[10]) 0x7fffffffdc40

(gdb) watch *(int(*)[16])0x7fffffffdc40
Hardware watchpoint 2: *(int(*)[16])0x7fffffffdc40

(gdb) continue
Continuing.

Hardware watchpoint 2: *(int(*)[16])0x7fffffffdc40

Old value = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4195904, 0, 0, 0, 0}
New value = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}

corrupt_memory () at corrupt.c:16
16          for (int i = 0; i <= 15; i++) {

案例:Use-After-Free导致的踩踏

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

void use_after_free(){
char* ptr1 = (char*)malloc(16);
char* ptr2 = (char*)malloc(16);

printf(“ptr1 = %p\n“, ptr1);
printf(“ptr2 = %p\n“, ptr2);

strcpy(ptr1, “Data1“);
strcpy(ptr2, “Data2“);

free(ptr1);  // 释放ptr1

// 错误:继续使用已释放的内存
printf(“ptr1 after free: %s\n“, ptr1);  // 可能崩溃或输出错误数据

// 更危险:写入已释放的内存
strcpy(ptr1, “Corrupted!“);
printf(“ptr2 = %s\n“, ptr2);  // ptr2可能已被踩踏!

free(ptr2);
}

int main(){
    use_after_free();
return 0;
}

GDB分析:

# 使用AddressSanitizer编译
gcc -g -fsanitize=address -fno-omit-frame-pointer -o uaf use_after_free.c

# 运行程序
./uaf

ASan输出:

=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
READ of size 7 at 0x602000000010 thread T0
#0 0x400b3a in use_after_free use_after_free.c:18
#1 0x400c82 in main use_after_free.c:26

0x602000000010 is located 0 bytes inside of 16-byte region [0x602000000010,0x602000000020)
freed by thread T0 here:
#0 0x7f8a1b0d1b40 in free (/lib/x86_64-linux-gnu/libasan.so.5+0x10fb40)
#1 0x400b2d in use_after_free use_after_free.c:17

previously allocated by thread T0 here:
#0 0x7f8a1b0d1b40 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x10fb40)
#1 0x400b1a in use_after_free use_after_free.c:13
=================================================================

内存问题的预防策略

1. 使用安全函数

// 不安全的函数
char dest[10];
strcpy(dest, “This is too long“);  // 缓冲区溢出

// 安全的函数
strncpy(dest, “This is too long“, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = ‘\0’;  // 确保字符串终止

2. 使用智能指针(C++)

#include<memory>
#include<vector>

// 使用unique_ptr自动管理内存
void safe_memory(){
auto ptr = std::make_unique<int[]>(100);
    ptr[0] = 42;

// 自动释放,无需手动delete
}

// 使用vector替代动态数组
void safe_vector(){
std::vector<int> v;
    v.resize(100);
    v[99] = 42;  // 边界检查(debug模式下)
// 自动释放内存
}

3. 编译时检查选项

# GCC/Clang编译选项
gcc -g -O0 \
    -fstack-protector-all \
    -fno-omit-frame-pointer \
    -D_FORTIFY_SOURCE=2 \
    -Wformat -Wformat-security \
    -Wall -Wextra \
    your_program.c

4. 静态分析工具

# 使用cppcheck
cppcheck --enable=all your_program.c

# 使用clang-tidy
clang-tidy your_program.c -- -I/usr/include

# 使用AddressSanitizer(运行时)
gcc -g -fsanitize=address your_program.c

四、Sanitizer工具应用:现代C++调试的利器

AddressSanitizer (ASan)工作原理

核心机制:影子内存(Shadow Memory)

ASan通过影子内存技术实现高效的内存错误检测:

  • 程序每8字节的用户内存对应1字节的影子内存
  • 影子字节记录该内存区域的“可访问状态”
  • 所有内存访问指令在编译时被插桩,自动检查影子字节
  • 检测到非法访问时立即报告

检测能力:

  • 堆/栈/全局缓冲区溢出
  • Use-After-Free(使用已释放的内存)
  • Use-After-Return(使用已返回的栈内存)
  • Double-Free(重复释放)
  • 内存泄漏(配合LeakSanitizer)

ASan使用示例

// heap_overflow.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int main(){
char* buffer = (char*)malloc(10);
if (!buffer) return 1;

// 越界写入
strcpy(buffer, “This string is too long!“);

free(buffer);
return 0;
}

编译与运行:

# 编译启用ASan
gcc -g -fsanitize=address -fno-omit-frame-pointer -o heap_overflow heap_overflow.c

# 运行程序
./heap_overflow

ASan输出:

=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000010
WRITE of size 27 at 0x602000000010 thread T0
#0 0x7f8a1b0d1b40 in strcpy (/lib/x86_64-linux-gnu/libasan.so.5+0x10fb40)
#1 0x400b3a in main heap_overflow.c:11

0x602000000010 is located 0 bytes to the right of 10-byte region [0x602000000006,0x602000000010)
allocated by thread T0 here:
#0 0x7f8a1b0d1b40 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x10fb40)
#1 0x400b1a in main heap_overflow.c:8
=================================================================

ASan高级配置

# 环境变量配置
export ASAN_OPTIONS=detect_leaks=1:halt_on_error=1

# 检测栈使用后返回
export ASAN_OPTIONS=detect_stack_use_after_return=1

# 设置分配器行为
export ASAN_OPTIONS=allocator_may_return_null=1

# 禁用某些检测(提高性能)
export ASAN_OPTIONS=intercept_tls_get_addr=0

ThreadSanitizer (TSan)使用方法

核心能力:

  • 数据竞争检测
  • 错误使用互斥锁
  • 条件变量使用不当
  • 释放后使用(线程场景)

TSan使用示例

// data_race.c
#include<stdio.h>
#include<pthread.h>

int shared_data = 0;

void* writer_thread(void* arg){
for (int i = 0; i < 100000; i++) {
        shared_data++;  // 无保护的写操作
    }
return NULL;
}

void* reader_thread(void* arg){
for (int i = 0; i < 100000; i++) {
printf(“Data: %d\n“, shared_data);  // 无保护的读操作
    }
return NULL;
}

int main(){
pthread_t writer, reader;

    pthread_create(&writer, NULL, writer_thread, NULL);
    pthread_create(&reader, NULL, reader_thread, NULL);

    pthread_join(writer, NULL);
    pthread_join(reader, NULL);

return 0;
}

编译与运行:

# 编译启用TSan
gcc -g -fsanitize=thread -fno-omit-frame-pointer -O1 -o data_race data_race.c -lpthread

# 运行程序
./data_race

TSan输出:

==================
WARNING: ThreadSanitizer: data race (pid=12345)
Write of size 4 at 0x555555558010 by thread T1:
#0 0x400b3a in writer_thread data_race.c:11
#1 0x7f8a1b0d1b40 in pthread_create (/lib/x86_64-linux-gnu/libtsan.so.0+0x10fb40)

Previous read of size 4 at 0x555555558010 by thread T2:
#0 0x400b5a in reader_thread data_race.c:17
#1 0x7f8a1b0d1b40 in pthread_create (/lib/x86_64-linux-gnu/libtsan.so.0+0x10fb40)

Location is global ‘shared_data‘ of size 4 at 0x555555558010
Thread T1 (tid=12347, running) created by main thread at:
#0 pthread_create data_race.c:24
Thread T2 (tid=12348, running) created by main thread at:
#0 pthread_create data_race.c:25
==================

TSan高级技巧

# 环境变量配置
export TSAN_OPTIONS=“halt_on_error=1:second_deadlock_stack=1“

# 输出竞争详情
export TSAN_OPTIONS=“history_size=7“

# 抑制特定警告
echo “race:my_function:*ignore“ > tsan.supp
export TSAN_OPTIONS=“suppressions=./tsan.supp“

修复数据竞争

#include<pthread.h>

int shared_data = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* writer_thread(void* arg){
for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);
        shared_data++;  // 加锁保护
        pthread_mutex_unlock(&mutex);
    }
return NULL;
}

void* reader_thread(void* arg){
int temp;
for (int i = 0; i < 100000; i++) {
        pthread_mutex_lock(&mutex);
        temp = shared_data;  // 加锁保护
        pthread_mutex_unlock(&mutex);
printf(“Data: %d\n“, temp);
    }
return NULL;
}

Sanitizer组合使用策略

不能同时使用的组合:

  • ASan + TSan(冲突)
  • ASan + MSan(冲突)
  • TSan + MSan(冲突)

推荐组合:

# 日常调试:ASan + UBSan
gcc -g -fsanitize=address,undefined -fno-omit-frame-pointer -O1 your_program.c

# 多线程专项:TSan
gcc -g -fsanitize=thread -fno-omit-frame-pointer -O1 your_program.c -lpthread

# 内存专项:ASan + LSan(LSan默认包含在ASan中)
gcc -g -fsanitize=address -fno-omit-frame-pointer -O1 your_program.c

# CI/CD流程:分任务运行
# Job 1: ASan测试
# Job 2: TSan测试
# Job 3: UBSan测试

掌握这些GDB调试与Sanitizer工具的高级技巧,能让你在面对复杂的C/C++程序崩溃、死锁和内存问题时,不再手足无措。从核心转储分析到运行时内存检测,构建起一套完整的调试与问题定位体系,是每一位追求卓越的开发者的必备技能。如果你在实践中有更多心得,欢迎到云栈社区与大家交流探讨。




上一篇:FastAPI开发10个高频问题与解决方案:类型校验、异步、部署避坑指南
下一篇:从Skill开发到架构设计:AI应用开发中的微服务式实践与感悟
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 02:48 , Processed in 0.252811 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表