glog3 捕获Windows异常崩溃信号

文章目录

  • 实际演示代码
  • [g3log 捕获 Windows 异常崩溃信号完全指南](#g3log 捕获 Windows 异常崩溃信号完全指南)
    • 一、概述
    • [二、Windows 崩溃捕获机制](#二、Windows 崩溃捕获机制)
      • [2.1 两层异常处理架构](#2.1 两层异常处理架构)
      • [2.2 捕获的致命信号](#2.2 捕获的致命信号)
      • [2.3 捕获的 Windows 结构化异常](#2.3 捕获的 Windows 结构化异常)
    • 三、实现原理深度解析
      • [3.1 信号处理器的安装](#3.1 信号处理器的安装)
      • [3.2 信号处理流程](#3.2 信号处理流程)
      • [3.3 异常处理流程](#3.3 异常处理流程)
      • [3.4 崩溃安全的保证](#3.4 崩溃安全的保证)
    • 四、配置选项
      • [4.1 CMake 编译选项](#4.1 CMake 编译选项)
      • [4.2 运行时控制](#4.2 运行时控制)
    • 五、完整示例
    • 六、最佳实践
      • [6.1 多线程注意事项](#6.1 多线程注意事项)
      • [6.2 调试与发布的区别](#6.2 调试与发布的区别)
      • [6.3 自定义崩溃行为](#6.3 自定义崩溃行为)
    • 七、总结

实际演示代码

cpp 复制代码
// 关闭CRT安全警告
#define _CRT_SECURE_NO_WARNINGS

// 防止Windows宏污染 std::min std::max
#define NOMINMAX

// 精简Windows头文件
#define WIN32_LEAN_AND_MEAN

// Windows API
#include <windows.h>

// Windows 调试符号 API(用于堆栈解析)
#include <DbgHelp.h>

// C 信号处理
#include <csignal>

// C++ 异常
#include <exception>

// 控制台输出
#include <iostream>

// C++标准异常
#include <stdexcept>

// 浮点控制
#include <float.h>

// 内存分配
#include <new>

// RTTI
#include <typeinfo>

// 防止Windows宏污染
#undef ERROR
#undef WARNING
#undef DEBUG

// g3log日志库
#include <g3log/g3log.hpp>
#include <g3log/logworker.hpp>

// 链接 Windows 调试库
#pragma comment(lib, "DbgHelp.lib")


//================================================
// StackTrace
//================================================

// 打印堆栈信息
void PrintStackTrace(CONTEXT* ctx)
{
    // 获取当前进程句柄
    HANDLE process = GetCurrentProcess();

    // 获取当前线程
    HANDLE thread = GetCurrentThread();

    // 初始化符号解析
    SymInitialize(process, NULL, TRUE);

    // 堆栈帧结构
    STACKFRAME64 frame{};

    // CPU架构
    DWORD machine;

#ifdef _M_X64

    // 64位架构
    machine = IMAGE_FILE_MACHINE_AMD64;

    // 当前指令地址
    frame.AddrPC.Offset = ctx->Rip;
    frame.AddrPC.Mode = AddrModeFlat;

    // 帧指针
    frame.AddrFrame.Offset = ctx->Rbp;
    frame.AddrFrame.Mode = AddrModeFlat;

    // 栈指针
    frame.AddrStack.Offset = ctx->Rsp;
    frame.AddrStack.Mode = AddrModeFlat;

#else

    // 32位架构
    machine = IMAGE_FILE_MACHINE_I386;

    frame.AddrPC.Offset = ctx->Eip;
    frame.AddrPC.Mode = AddrModeFlat;

    frame.AddrFrame.Offset = ctx->Ebp;
    frame.AddrFrame.Mode = AddrModeFlat;

    frame.AddrStack.Offset = ctx->Esp;
    frame.AddrStack.Mode = AddrModeFlat;

#endif

    // 打印堆栈开始
    LOG(FATAL) << "=========== STACK TRACE ===========";

    // 最多打印50层堆栈
    for (int i = 0; i < 50; i++)
    {
        // 进行一次堆栈回溯
        if (!StackWalk64(machine, process, thread, &frame, ctx, NULL,
            SymFunctionTableAccess64, SymGetModuleBase64, NULL))
            break;

        // 获取当前地址
        DWORD64 addr = frame.AddrPC.Offset;

        // 地址为0说明结束
        if (addr == 0) break;

        // 符号缓冲区
        char buffer[sizeof(SYMBOL_INFO) + 256];

        // 转换为符号结构
        PSYMBOL_INFO symbol = (PSYMBOL_INFO)buffer;

        // 设置结构大小
        symbol->SizeOfStruct = sizeof(SYMBOL_INFO);

        // 最大符号名长度
        symbol->MaxNameLen = 255;

        DWORD64 displacement;

        // 尝试解析符号
        if (SymFromAddr(process, addr, &displacement, symbol))
        {
            LOG(FATAL) << i << ": " << symbol->Name
                << " - 0x" << std::hex << symbol->Address;
        }
        else
        {
            LOG(FATAL) << i << ": 0x" << std::hex << addr;
        }
    }

    // 结束
    LOG(FATAL) << "===================================";
}


//================================================
// Exception Name
//================================================

// 根据异常代码返回字符串
const char* ExceptionName(DWORD code)
{
    switch (code)
    {
    case EXCEPTION_ACCESS_VIOLATION: return "ACCESS_VIOLATION";
    case EXCEPTION_ARRAY_BOUNDS_EXCEEDED: return "ARRAY_BOUNDS_EXCEEDED";
    case EXCEPTION_BREAKPOINT: return "BREAKPOINT";
    case EXCEPTION_DATATYPE_MISALIGNMENT: return "DATATYPE_MISALIGNMENT";
    case EXCEPTION_FLT_DIVIDE_BY_ZERO: return "FLOAT_DIVIDE_BY_ZERO";
    case EXCEPTION_ILLEGAL_INSTRUCTION: return "ILLEGAL_INSTRUCTION";
    case EXCEPTION_IN_PAGE_ERROR: return "IN_PAGE_ERROR";
    case EXCEPTION_INT_DIVIDE_BY_ZERO: return "INT_DIVIDE_BY_ZERO";
    case EXCEPTION_STACK_OVERFLOW: return "STACK_OVERFLOW";
    default: return "UNKNOWN_EXCEPTION";
    }
}


//================================================
// SEH Handler
//================================================

// Windows 未捕获异常处理函数
LONG WINAPI SehHandler(EXCEPTION_POINTERS* e)
{
    // 获取异常码
    DWORD code = e->ExceptionRecord->ExceptionCode;

    LOG(FATAL) << "========== CRASH ==========";

    LOG(FATAL) << "Exception: " << ExceptionName(code);

    LOG(FATAL) << "Code: 0x" << std::hex << code;

    LOG(FATAL) << "Address: " << e->ExceptionRecord->ExceptionAddress;

    // 打印堆栈
    PrintStackTrace(e->ContextRecord);

    // 告诉系统异常已处理
    return EXCEPTION_EXECUTE_HANDLER;
}


//================================================
// Vectored Handler
//================================================

// Windows 向量异常处理
LONG CALLBACK VectoredHandler(PEXCEPTION_POINTERS e)
{
    DWORD code = e->ExceptionRecord->ExceptionCode;

    LOG(FATAL) << "Vectored exception caught: 0x"
        << std::hex << code;

    return EXCEPTION_CONTINUE_SEARCH;
}


//================================================
// terminate
//================================================

// C++ terminate 处理函数
void TerminateHandler()
{
    LOG(FATAL) << "std::terminate called";

    // 获取当前异常
    auto exptr = std::current_exception();

    if (exptr)
    {
        try
        {
            // 重新抛出异常
            std::rethrow_exception(exptr);
        }
        catch (const std::exception& e)
        {
            LOG(FATAL) << "Unhandled C++ exception: " << e.what();
        }
        catch (...)
        {
            LOG(FATAL) << "Unknown C++ exception";
        }
    }

    abort();
}


//================================================
// Pure virtual
//================================================

// 纯虚函数调用异常
void PureCallHandler()
{
    LOG(FATAL) << "Pure virtual function called!";

    abort();
}


//================================================
// Signal
//================================================

// C信号处理
void SignalHandler(int sig)
{
    LOG(FATAL) << "Signal caught: " << sig;

    abort();
}


//================================================
// Install handlers
//================================================

// 安装所有崩溃捕获
void InstallCrashHandler()
{
    // Windows向量异常
    AddVectoredExceptionHandler(1, VectoredHandler);

    // 未处理异常
    SetUnhandledExceptionFilter(SehHandler);

    // C++ terminate
    std::set_terminate(TerminateHandler);

    // pure virtual call
    _set_purecall_handler(PureCallHandler);

    // C signal
    signal(SIGABRT, SignalHandler);
    signal(SIGSEGV, SignalHandler);
    signal(SIGFPE, SignalHandler);
    signal(SIGILL, SignalHandler);
}


//================================================
// Crash test functions grouped by categories
//================================================

namespace AccessViolationTests {
    // 空指针写
    void CrashAccessViolation() {
        int* p = nullptr;
        *p = 1;
    }

    // 非法地址写
    void CrashBadWrite() {
        int* p = (int*)0x123;
        *p = 10;
    }

    // 非法函数调用
    void CrashInvalidCall() {
        typedef void(*Func)();
        Func f = (Func)0x12345678;
        f();
    }
}

namespace DivideByZeroTests {
    // 整数除0
    void CrashDivideByZero() {
        volatile int a = 1;
        volatile int b = 0;
        volatile int c = a / b;
    }
}

namespace IllegalInstructionTests {
    // 非法指令
    void CrashIllegalInstruction() {
#if defined(_MSC_VER) && defined(_M_X64)
        __ud2();
#endif
    }
}

namespace StackTests {
    // 栈溢出
    void StackOverflow() {
        StackOverflow();
    }

    // 栈缓冲区溢出(触发栈保护页或访问违例)
    void CrashStackBufferOverflow() {
        volatile char buffer[10];
        for (int i = 0; i <= 20; ++i) {
            buffer[i] = static_cast<char>(i);
        }
    }
}

namespace AbortTests {
    // abort崩溃
    void CrashAbort() {
        abort();
    }
}

namespace CppExceptionTests {
    // 未捕获C++异常
    void CrashCppException() {
        throw std::runtime_error("test exception");
    }

    // bad_alloc 异常
    void CrashBadAlloc() {
        size_t huge = ~(size_t)0;  // 最大可分配值
        void* p = ::operator new(huge);
    }

    // bad_cast 异常
    struct Base { virtual ~Base() {} };
    struct Derived : Base {};
    void CrashBadCast() {
        Base b;
        Derived& d = dynamic_cast<Derived&>(b);  // 抛出 std::bad_cast
    }

    // 在 noexcept 函数中抛出异常(触发 std::terminate)
    void CrashNoexceptThrow() noexcept {
        throw std::runtime_error("throw from noexcept");
    }
}

namespace HeapTests {
    // 堆损坏:写入已释放的内存
    void CrashHeapCorruption() {
        HANDLE heap = HeapCreate(0, 4096, 0);
        if (!heap) return;
        int* p = (int*)HeapAlloc(heap, 0, sizeof(int));
        if (p) {
            *p = 123;
            HeapFree(heap, 0, p);
            *p = 456;  // 访问已释放内存
        }
        HeapDestroy(heap);
    }

    // 双重释放
    void CrashDoubleFree() {
        HANDLE heap = HeapCreate(0, 4096, 0);
        if (!heap) return;
        int* p = (int*)HeapAlloc(heap, 0, sizeof(int));
        if (p) {
            HeapFree(heap, 0, p);
            HeapFree(heap, 0, p);  // 第二次释放
        }
        HeapDestroy(heap);
    }
}

namespace InvalidHandleTests {
    // 使用无效句柄
    void CrashInvalidHandle() {
        HANDLE handle = INVALID_HANDLE_VALUE;
        CloseHandle(handle);  // 关闭无效句柄可能引发异常
    }
}

namespace SignalTests {
    // 通过 raise 触发信号
    void CrashRaiseSigSegv() { raise(SIGSEGV); }
    void CrashRaiseSigAbort() { raise(SIGABRT); }
    void CrashRaiseSigFpe() { raise(SIGFPE); }
    void CrashRaiseSigIll() { raise(SIGILL); }
}

namespace FloatExceptionTests {
    // 浮点除零(需启用浮点异常)
    void CrashFloatDivideByZero() {
        unsigned int old = _controlfp(0, 0);
        _controlfp(_EM_ZERODIVIDE, _EM_ZERODIVIDE);
        volatile double a = 1.0, b = 0.0, c = a / b;
        _controlfp(old, _MCW_EM);  // 恢复
    }

    // 浮点溢出
    void CrashFloatOverflow() {
        unsigned int old = _controlfp(0, 0);
        _controlfp(_EM_OVERFLOW, _EM_OVERFLOW);
        volatile double a = 1e300, b = 1e300, c = a * b;
        _controlfp(old, _MCW_EM);
    }
}

namespace PureCallTests {
    //// 构造函数中调用纯虚函数(触发 purecall 处理)
    //class AbstractBase {
    //public:
    //    virtual void Pure() = 0;
    //    AbstractBase() { Pure(); }
    //};
    //class Derived2 : public AbstractBase {
    //public:
    //    virtual void Pure() override {}
    //};
    //void CrashPureCallFromConstructor() {
    //    Derived2 obj;
    //}
}

namespace RaiseExceptionTests {
    // 使用 RaiseException 触发自定义结构化异常
    void CrashRaiseException() {
        RaiseException(0xE0000001, 0, 0, NULL);
    }
}


//================================================
// main
//================================================

int main()
{
    // 创建g3log worker
    auto worker = g3::LogWorker::createLogWorker();

    // 初始化日志
    g3::initializeLogging(worker.get());

    // 安装崩溃处理
    InstallCrashHandler();

    LOG(INFO) << "Program started";

    // 修改 test 变量以选择不同的崩溃测试
    int test = 17;  // 例如:1=AccessViolation, 2=DivideByZero, ...

    switch (test)
    {
    case 1:  AccessViolationTests::CrashAccessViolation(); break;
    case 2:  DivideByZeroTests::CrashDivideByZero(); break;
    case 3:  IllegalInstructionTests::CrashIllegalInstruction(); break;
    case 4:  StackTests::StackOverflow(); break;
    case 5:  AbortTests::CrashAbort(); break;
    case 6:  AccessViolationTests::CrashBadWrite(); break;
    case 7:  CppExceptionTests::CrashCppException(); break;
    case 8:  AccessViolationTests::CrashInvalidCall(); break;
    case 9:  HeapTests::CrashHeapCorruption(); break;
    case 10: HeapTests::CrashDoubleFree(); break;
    case 11: StackTests::CrashStackBufferOverflow(); break;
    case 12: InvalidHandleTests::CrashInvalidHandle(); break;
    case 13: SignalTests::CrashRaiseSigSegv(); break;
    case 14: SignalTests::CrashRaiseSigAbort(); break;
    case 15: SignalTests::CrashRaiseSigFpe(); break;
    case 16: SignalTests::CrashRaiseSigIll(); break;
    case 17: FloatExceptionTests::CrashFloatDivideByZero(); break;
    case 18: FloatExceptionTests::CrashFloatOverflow(); break;
    case 19: CppExceptionTests::CrashBadAlloc(); break;
    case 20: CppExceptionTests::CrashBadCast(); break;
    case 21: CppExceptionTests::CrashNoexceptThrow(); break;
    //case 22: PureCallTests::CrashPureCallFromConstructor(); break;
    case 23: RaiseExceptionTests::CrashRaiseException(); break;
    default: LOG(INFO) << "No test selected"; break;
    }

    return 0;
}

g3log 捕获 Windows 异常崩溃信号完全指南

一、概述

g3log 是一个异步、崩溃安全的 C++ 日志库,由 KjellKod.cc 开发并作为公共领域代码发布 。它的三大核心特性包括:直观的 LOG API、设计契约式的 CHECK 功能,以及最重要的------崩溃时的优雅退出机制,确保不会丢失任何日志信息

在 Windows 平台上,g3log 提供了比 Linux 更全面的崩溃捕获机制。它不仅处理标准的致命信号(signals),还能捕获 Windows 特有的结构化异常(SEH)和向量化异常(Vectored Exceptions) 。

二、Windows 崩溃捕获机制

2.1 两层异常处理架构

g3log 在 Windows 上实现了双层防御的崩溃捕获架构:

层级 处理机制 捕获范围
第一层 向量化异常处理 (Vectored Exception Handling) 所有异常的第一站,优先于任何线程特定的处理程序
第二层 未处理异常过滤 (Unhandled Exception Filter) 未被向量化处理捕获或处理的异常

2.2 捕获的致命信号

g3log 默认捕获以下致命信号 :

信号 名称 触发场景
SIGABRT 中止信号 abort() 调用、LOG(FATAL)CHECK(false)
SIGFPE 浮点异常 整数除以零、浮点运算错误
SIGILL 非法指令 非法指令执行
SIGSEGV 段错误 空指针解引用、访问非法内存地址
SIGTERM 终止信号 外部进程终止请求

2.3 捕获的 Windows 结构化异常

除了标准信号,g3log 还捕获 Windows 特有的结构化异常 :

异常代码 异常名称 描述
EXCEPTION_ACCESS_VIOLATION 访问违例 相当于 SIGSEGV
EXCEPTION_ARRAY_BOUNDS_EXCEEDED 数组越界 数组访问超出边界
EXCEPTION_DATATYPE_MISALIGNMENT 数据类型错位 内存对齐错误
EXCEPTION_FLT_DIVIDE_BY_ZERO 浮点除零 浮点数除以零
EXCEPTION_INT_DIVIDE_BY_ZERO 整数除零 整数除以零
EXCEPTION_STACK_OVERFLOW 栈溢出 函数递归过深导致栈溢出

三、实现原理深度解析

3.1 信号处理器的安装

g3log 在 Windows 上有一个独特之处:SIGFPE、SIGILL 和 SIGSEGV 的处理必须在每个线程中单独安装 。这是 Windows 平台的限制,g3log 通过两种方式解决:

  1. 自动安装 :每次调用 LOG 宏时,会通过 SIGNAL_HANDLER_VERIFY() 宏确保当前线程安装了信号处理器
  2. 手动安装 :开发者可以调用 g3::installSignalHandlerForThread() 为任意线程显式安装
cpp 复制代码
// 自动安装机制(简化版)
#define SIGNAL_HANDLER_VERIFY() g3::installSignalHandlerForThread()

LogCapture::~LogCapture() {
    SIGNAL_HANDLER_VERIFY();  // 每个日志调用都会确保线程信号处理器就绪
    saveMessage(...);
}

3.2 信号处理流程

当致命信号发生时,g3log 的处理流程如下 :

  1. 信号触发 :操作系统调用注册的信号处理器 signalHandler()
  2. 堆栈捕获 :调用 stacktrace::stackdump() 生成当前线程的堆栈信息
  3. 日志记录 :创建 LogCapture 对象,将信号信息和堆栈写入日志流
  4. 调试中断 :在 Debug 模式下且启用了 DEBUG_BREAK_AT_FATAL_SIGNAL 宏时,触发 __debugbreak() 让 Visual Studio 中断
  5. 日志刷新LogCapture 析构时,将消息发送给后台工作线程,确保所有挂起的日志被写入
  6. 进程退出:恢复默认信号处理,再次引发原信号以终止进程

3.3 异常处理流程

对于 Windows 结构化异常,处理流程类似但略有不同 :

  1. 异常捕获:向量化异常处理器或未处理异常过滤器捕获异常
  2. 异常码转换:将 Windows 异常码转换为可读的异常名称
  3. 堆栈捕获 :从 EXCEPTION_POINTERS 结构中获取异常发生时的上下文,生成堆栈信息
  4. 日志记录:记录异常信息和堆栈
  5. 继续搜索 :返回 EXCEPTION_CONTINUE_SEARCH,允许其他异常处理器继续处理

3.4 崩溃安全的保证

g3log 的"崩溃安全"体现在 :

  • 发生崩溃时,所有已在队列中的日志消息都会被写入持久化存储
  • 日志写入发生在后台线程,不会阻塞崩溃的主线程
  • 只有当所有日志都处理完毕后,才允许进程真正退出

四、配置选项

4.1 CMake 编译选项

g3log 提供了多个与崩溃处理相关的 CMake 选项 :

选项 默认值 描述
ENABLE_FATAL_SIGNALHANDLING ON 启用/禁用致命信号处理
DISABLE_VECTORED_EXCEPTIONHANDLING OFF 禁用向量化异常处理
DEBUG_BREAK_AT_FATAL_SIGNAL OFF Debug 模式下在致命信号处触发调试中断

4.2 运行时控制

g3log 允许开发者自定义崩溃处理行为 :

  • 预崩溃钩子:注册在崩溃处理前执行的函数
  • 自定义退出处理:覆盖默认的退出行为

五、完整示例

以下是一个完整的 Windows 崩溃捕获示例,展示了 g3log 如何捕获各种类型的崩溃:

cpp 复制代码
#include <g3log/g3log.hpp>
#include <g3log/logworker.hpp>
#include <windows.h>

int main() {
    // 初始化 g3log
    auto worker = g3::LogWorker::createLogWorker();
    worker->addDefaultLogger("crash_test", "./");
    g3::initializeLogging(worker.get());
    
    LOG(INFO) << "程序启动,崩溃处理已就绪";
    
    // 测试各类崩溃
    int choice = 7;  // 选择测试类型
    
    switch(choice) {
        case 1:  // 空指针解引用 (SIGSEGV)
            LOG(INFO) << "测试: 空指针解引用";
            *(volatile int*)0 = 123;
            break;
            
        case 2:  // 整数除零 (SIGFPE)
            LOG(INFO) << "测试: 整数除零";
            volatile int a = 1, b = 0, c = a / b;
            break;
            
        case 3:  // LOG(FATAL) (SIGABRT)
            LOG(FATAL) << "主动触发致命错误";
            break;
            
        case 4:  // CHECK 失败 (SIGABRT)
            CHECK(1 == 2) << "条件失败";
            break;
            
        case 5:  // 栈溢出 (EXCEPTION_STACK_OVERFLOW)
            LOG(INFO) << "测试: 栈溢出";
            auto stack_overflow = [&]() { stack_overflow(); };
            stack_overflow();
            break;
            
        case 6:  // 非法指令 (SIGILL)
            LOG(INFO) << "测试: 非法指令";
            __ud2();  // x86/x64 非法指令
            break;
            
        case 7:  // 自定义结构化异常
            LOG(INFO) << "测试: 自定义异常";
            RaiseException(0xE0000001, 0, 0, nullptr);
            break;
    }
    
    return 0;
}

六、最佳实践

6.1 多线程注意事项

由于 Windows 对 SIGFPE、SIGILL、SIGSEGV 的要求,在多线程程序中需要注意:

  • 每个新创建的线程如果可能触发崩溃,建议调用 g3::installSignalHandlerForThread()
  • 或者确保每个线程至少执行一次 LOG 调用,自动触发信号处理器安装

6.2 调试与发布的区别

  • Debug 模式 :可以启用 DEBUG_BREAK_AT_FATAL_SIGNAL,让崩溃时自动在 Visual Studio 中断,方便调试
  • Release 模式:建议关闭此选项,让程序在崩溃时干净地退出并记录日志

6.3 自定义崩溃行为

如需在崩溃时执行自定义操作(如发送错误报告),可以通过以下方式:

cpp 复制代码
// 注册预崩溃钩子(需要查看具体 API 文档)
g3::setFatalPreLoggingHook([]() {
    // 发送错误报告、保存临时文件等
    MessageBox(nullptr, "程序即将崩溃", "错误", MB_OK);
});

七、总结

g3log 在 Windows 平台上提供了全面的崩溃捕获能力:

  • ✅ 捕获标准致命信号(SIGABRT、SIGFPE、SIGILL、SIGSEGV、SIGTERM)
  • ✅ 捕获 Windows 结构化异常(访问违例、除零、栈溢出等)
  • ✅ 生成详细的堆栈信息,帮助定位问题
  • ✅ 确保崩溃前所有日志都被写入,不丢失信息
  • ✅ 支持向量化异常处理,优先捕获异常
  • ✅ 提供灵活的配置选项和扩展接口

通过合理使用 g3log 的崩溃捕获机制,开发者可以大大提高 Windows 应用程序的可靠性和可诊断性,让那些难以复现的偶发崩溃变得有迹可循。

相关推荐
易水寒陈2 小时前
单片机的命令模式
单片机·命令模式
集芯微电科技有限公司2 小时前
700V/1.6A单通道GaN FET增强型驱动器具有零反向恢复损耗
人工智能·单片机·嵌入式硬件·深度学习·神经网络·机器学习·生成对抗网络
咔咔门2 小时前
Windows 配置 chatExcel-MCP完整踩坑指南
windows
承前智2 小时前
Arduino1.8.19与stm32+ESP32的geek卸载及环境安装
stm32·单片机·嵌入式硬件
bu_shuo2 小时前
Windows电脑使用VSCode远程控制Windows主机方法记录
windows·vscode·ssh·powershell
全栈游侠2 小时前
STM32F103XX 05-时钟配置分析与自举程序
stm32·单片机·嵌入式硬件
学嵌入式的小杨同学2 小时前
STM32 入门封神之路(四):GPIO 实战 + 寄存器深度拆解 ——LED 控制 + 按键检测全流程(含位操作 + 面试题)
stm32·单片机·嵌入式硬件·硬件架构·硬件工程·智能硬件·嵌入式实时数据库
撩妹小狗3 小时前
定时器PWM输出功能的使用
单片机·嵌入式硬件
咖啡续命又一天4 小时前
PHP 8.2 (Windows) 安装 Redis 扩展最新教程
windows·redis·php