windows驱动开发-内核编程技术汇总(四)

编写Bug检查原因回调例程

驱动程序可以选择性地提供 KBUGCHECK_REASON_CALLBACK_ROUTINE 回调函数,系统在写入故障转储文件后调用该函数。

在此回调中,驱动程序可以:

  • 将特定于驱动程序的数据添加到故障转储文件;
  • 将设备重置为已知状态;

使用以下例程注册和删除回调:

  • KeRegisterBugCheckReasonCallback
  • KeDeregisterBugCheckReasonCallback

此回调类型是重载的,其行为会根据注册时提供的 KBUGCHECK_CALLBACK_REASON 常量值而更改。

Bug 检查回调例程限制

bug 检查回调例程在 IRQL = HIGH_LEVEL 处执行,这对其可执行的操作施加了严格的限制。

bug 检查回调例程无法:

  • 分配内存;
  • 访问可分页内存;
  • 使用任何同步机制;
  • 调用必须在 IRQL = DISPATCH_LEVEL 或更低位置执行的任何例程;

Bug 检查回调例程保证运行而不会中断,因此无需同步。 如果 bug 检查例程尝试使用任何同步机制获取锁,系统会死锁。请记住,在检查 bug 时,数据结构或列表可能处于不一致状态,因此在访问受锁保护的数据结构时应小心。 例如,在浏览列表时,应添加上限检查,并验证链接是否指向有效内存,以防有循环列表或链接指向无效地址。

Bug 检查回调例程可以使用 MmIsAddressValid 来检查访问地址是否会导致页面错误。 由于例程在没有中断的情况下运行,并且其他核心被冻结,这满足了该函数的同步要求。 在 Bug 检查回调中延迟它们之前,应始终使用 MmIsAddressValid 检查可能分页或无效的内核地址进行检查,因为页面错误将导致双重错误,并可能阻止写入转储。

驱动程序的 bug 检查回调例程可以安全地使用 READ_PORT_XXX、READ_REGISTER_XXX、WRITE_PORT_XXX 和 WRITE_REGISTER_XXX 例程与驱动程序的设备通信。

实现 KbCallbackAddPages 回调例程

内核模式驱动程序可以实现 KbCallbackAddPages 类型的KBUGCHECK_REASON_CALLBACK_ROUTINE回调函数,以在 bug 检查发生时向故障转储文件添加一页或多页数据。 若要向操作系统注册此例程,驱动程序会调用 KeRegisterBugCheckReasonCallback 例程。 在驱动程序卸载之前,它必须调用 KeDeregisterBugCheckReasonCallback 例程来删除注册。

从 Windows 8 开始,在内核内存转储或完整内存转储期间调用已注册的 KbCallbackAddPages 例程。 在早期版本的 Windows 中,注册的 KbCallbackAddPages 例程在内核内存转储期间调用,但不在完整内存转储期间调用。 默认情况下,内核内存转储仅包含发生 bug 检查时 Windows 内核正在使用的物理页,而完整的内存转储则包含 Windows 使用的所有物理内存。 默认情况下,完整内存转储不包括平台固件使用的物理内存。

KbCallbackAddPages 例程可以提供特定于驱动程序的数据,以添加到转储文件。 例如,对于内核内存转储,此附加数据可以包括未映射到虚拟内存中的系统地址范围但包含有助于调试驱动程序的信息的物理页。 KbCallbackAddPages 例程可能会将未映射或映射到虚拟内存中用户模式地址的任何驱动程序拥有的物理页面添加到转储文件。

当出现 bug 检查时,操作系统会调用所有已注册的 KbCallbackAddPages 例程,以轮询驱动程序以查找要添加到故障转储文件中的数据。 每次调用都会将一页或多页连续数据添加到故障转储文件。 KbCallbackAddPages 例程可以为起始页提供虚拟地址或物理地址。 如果在调用期间提供了多个页面,则这些页面在虚拟内存或物理内存中是连续的,具体取决于起始地址是虚拟地址还是物理地址。 若要提供不连续的页面, KbCallbackAddPages 例程可以在 KBUGCHECK_ADD_PAGES 结构中设置一个标志,以指示它具有其他数据,并且必须再次调用。

与向次要故障转储区域追加数据的 KbCallbackSecondaryDumpData 例程不同, KbCallbackAddPages 例程会将数据页添加到主要故障转储区域。 在调试期间,主要故障转储数据比辅助故障转储数据更易于访问。

操作系统填充 ReasonSpecificData 指向的 KBUGCHECK_ADD_PAGES 结构的 BugCheckCode 成员。 KbCallbackAddPages 例程必须设置此结构的标志、地址和计数成员的值。

在首次调用 KbCallbackAddPages 之前,操作系统会将 Context 初始化为 NULL。 如果多次调用 KbCallbackAddPages 例程,操作系统将保留回调例程在上一次调用中写入上下文的成员的值。

KbCallbackAddPages 例程在可以执行的操作方面非常有限。

实现 KbCallbackDumpIo 回调例程

内核模式驱动程序可以实现 KbCallbackDumpIo 类型的KBUGCHECK_REASON_CALLBACK_ROUTINE回调函数,以在每次将数据写入故障转储文件时执行工作。 系统会在 ReasonSpecificData 参数中传递指向 KBUGCHECK_DUMP_IO 结构的指针。 Buffer 成员指向当前数据,BufferLength 成员指定其长度。 Type 成员指示当前正在写入的数据类型,例如转储文件头信息、内存状态或驱动程序提供的数据。 有关可能的信息类型的说明,请参阅 KBUGCHECK_DUMP_IO_TYPE 枚举。

系统可以按顺序或无序写入故障转储文件。 如果系统按顺序写入故障转储文件,则 ReasonSpecificData 的 Offset 成员为 -1;否则,Offset 设置为故障转储文件中的当前以字节为单位的偏移量。

当系统按顺序写入文件时, 在编写类型 = KbDumpIoHeader () 的标头信息时,它会调用每个 KbCallbackDumpIo 例程一次或多次,在编写故障转储文件的main正文 (Type = KbDumpIoBody) 时调用一次或多次, (Type = KbDumpIoSecondaryDumpData) 编写辅助转储数据时调用一次或多次。 系统完成编写故障转储文件后,会调用 Buffer = NULL、 BufferLength = 0 和 Type = KbDumpIoComplete 的回调。

KbCallbackDumpIo 例程main目的是允许将系统故障转储数据写入磁盘以外的设备。 例如,监视系统状态的设备可以使用回调来报告系统检查发出 bug,并提供故障转储以供分析。

使用 KeRegisterBugCheckReasonCallback 注册 KbCallbackDumpIo 例程。 驱动程序随后可以使用 KeDeregisterBugCheckReasonCallback 例程删除回调。 如果驱动程序可以卸载,则必须删除其 DRIVER_UNLOAD 回调函数中的任何已注册回调。

实现 KbCallbackSecondaryDumpData 回调例程

内核模式驱动程序可以实现 KbCallbackSecondaryDumpData 类型的KBUGCHECK_REASON_CALLBACK_ROUTINE回调函数,以提供要追加到故障转储文件的数据。

系统设置 ReasonSpecificData 指向的KBUGCHECK_SECONDARY_DUMP_DATA结构的 InBuffer、InBufferLength、OutBuffer 和 MaximumAllowed 成员。 MaximumAllowed 成员指定例程可以提供的最大转储数据量。

OutBuffer 成员的值确定系统是请求驱动程序转储数据的大小还是数据本身的大小,如下所示:

  • 如果 KBUGCHECK_SECONDARY_DUMP_DATA 的 OutBuffer 成员为 NULL,则系统仅请求大小信息。 KbCallbackSecondaryDumpData 例程填充 OutBuffer 和 OutBufferLength 成员;
  • 如果 KBUGCHECK_SECONDARY_DUMP_DATA 的 OutBuffer 成员等于 InBuffer 成员,则系统正在请求驱动程序的辅助转储数据。 KbCallbackSecondaryDumpData 例程填充 OutBuffer 和 OutBufferLength 成员,并将数据写入 OutBuffer 指定的缓冲区;

KBUGCHECK_SECONDARY_DUMP_DATA 的 InBuffer 成员指向一个小缓冲区供例程使用。 InBufferLength 成员指定缓冲区的大小。 如果要写入的数据量小于 InBufferLength,则回调例程可以使用此缓冲区向系统提供故障转储数据。 然后,回调例程将 OutBuffer 设置为 InBuffer , 将 OutBufferLength 设置为写入缓冲区的实际数据量。

必须写入大于 InBufferLength 的数据量的驱动程序可以使用其自己的缓冲区来提供数据。 必须在执行回调例程之前分配此缓冲区,并且必须驻留在常驻内存 (,例如非分页池) 。 然后,回调例程将 OutBuffer 设置为指向驱动程序的缓冲区,将 OutBufferLength 设置为缓冲区中要写入故障转储文件的数据量。

要写入故障转储文件的每个数据块都使用 KBUGCHECK_SECONDARY_DUMP_DATA 结构的 Guid 成员的值进行标记。 使用的 GUID 对于驱动程序必须是唯一的。 若要显示与此 GUID 对应的辅助转储数据,可以在调试器扩展中使用 .enumtag 命令或 IDebugDataSpaces3::ReadTagged 方法。 

驱动程序可以将具有相同 GUID 的多个块写入故障转储文件,但这种做法非常糟糕,因为调试器只能访问第一个块。 注册多个 KbCallbackSecondaryDumpData 例程的驱动程序应为每个回调分配唯一的 GUID。

使用 KeRegisterBugCheckReasonCallback 注册 KbCallbackSecondaryDumpData 例程。 驱动程序随后可以使用 KeDeregisterBugCheckReasonCallback 例程删除回调例程。 如果驱动程序可以卸载,则必须删除其 DRIVER_UNLOAD 回调函数中的任何已注册回调例程。

实现 KbCallbackTriageDumpData 回调例程

从 Windows 10 版本 1809 和 Windows Server 2019 开始,内核模式驱动程序可以实现 KbCallbackTriageDumpData 类型的KBUGCHECK_REASON_CALLBACK_ROUTINE回调函数,以标记虚拟内存范围以包含在新使用的内核微型转储中。 这可确保小型转储将包含指定的范围,以便可以使用在内核转储中工作的相同调试器命令来访问它们。 目前,这是针对win 10的小型转储实现的,这意味着捕获了内核或更大的转储,然后从较大的转储创建了一个小型转储。 默认情况下,大多数系统都针对自动/内核转储进行配置,并且系统会在崩溃后下次启动时自动创建一个小型转储。

系统在 ReasonSpecificData 参数中传递指向 KBUGCHECK_TRIAGE_DUMP_DATA 结构的指针,该结构包含有关 Bug 检查的信息,以及驱动程序用来返回其初始化和填充的数据数组的 OUT 参数。

在下面的示例中,驱动程序配置会审转储数组,然后注册回调的最小实现。 驱动程序将使用 数组将两个全局变量添加到小型转储:

#include <ntosp.h>

// Header definitions


    //
    // The maximum count of ranges the driver will add to the array.
    // This example is only adding max 3 ranges with some extra.
    //

#define MAX_RANGES 10

    //
    // This should be large enough to hold the maximum number of KADDRESS_RANGE
    // which the driver expects to add to the array.
    //

#define ARRAY_SIZE ((FIELD_OFFSET(KTRIAGE_DUMP_DATA_ARRAY, Blocks)) + (sizeof(KADDRESS_RANGE) * MAX_RANGES))

// Globals 
 
static PKBUGCHECK_REASON_CALLBACK_RECORD gBugcheckTriageCallbackRecord; 
static PKTRIAGE_DUMP_DATA_ARRAY gTriageDumpDataArray;

    //
    // This is a global variable which the driver wants to be available in
    // the kernel minidump. A real driver may add more address ranges.
    //

ULONG64 gDriverData1 = 0xAAAAAAAA;
PULONG64 gpDriverData2;
 
// Functions
 
VOID 
ExampleBugCheckCallbackRoutine( 
    KBUGCHECK_CALLBACK_REASON Reason, 
    PKBUGCHECK_REASON_CALLBACK_RECORD Record, 
    PVOID Data, 
    ULONG Length 
    ) 
{ 
    PKBUGCHECK_TRIAGE_DUMP_DATA DumpData; 
 
    UNREFERENCED_PARAMETER(Reason);
    UNREFERENCED_PARAMETER(Record);
    UNREFERENCED_PARAMETER(Length);

    DumpData = (PKBUGCHECK_TRIAGE_DUMP_DATA) Data;

    if ((DumpData->Flags & KB_TRIAGE_DUMP_DATA_FLAG_BUGCHECK_ACTIVE) == 0) {
        return;
    }

    if (gTriageDumpDataArray == NULL)
    {
        return;
    }
 
    //
    // Add the dynamically allocated global pointer and buffer once validated.
    //

    if ((gpDriverData2 != NULL) && (MmIsAddressValid(gpDriverData2))) {

        //
        // Add the address of the global itself a well as the pointed data
        // so you can use the global to access the data in the debugger
        // by running a command like "dt example!gpDriverData2"
        //

        KeAddTriageDumpDataBlock(gTriageDumpDataArray, &gpDriverData2, sizeof(PULONG64));
        KeAddTriageDumpDataBlock(gTriageDumpDataArray, gpDriverData2, sizeof(ULONG64));
    }

    //
    // Pass the array back for processing.
    //
 
    DumpData->DataArray = gTriageDumpDataArray; 
 
    return; 
}

// Setup Function

NTSTATUS
SetupTriageDataCallback(VOID) 
{ 
    PVOID pBuffer;
    NTSTATUS Status;
    BOOLEAN bSuccess;
 
    //
    // Call this function from DriverEntry.
    // 
    // Allocate a buffer to hold a callback record and triage dump data array
    // in the non-paged pool. 
    //
 
    pBuffer = ExAllocatePoolWithTag(NonPagedPoolNx,
                                    sizeof(KBUGCHECK_REASON_CALLBACK_RECORD) + ARRAY_SIZE,
                                    'Xmpl');

    if (pBuffer == NULL) {
        return STATUS_NO_MEMORY;
    }

    RtlZeroMemory(pBuffer, sizeof(KBUGCHECK_REASON_CALLBACK_RECORD));
    gBugcheckTriageCallbackRecord = (PKBUGCHECK_REASON_CALLBACK_RECORD) pBuffer;
    KeInitializeCallbackRecord(gBugcheckTriageCallbackRecord); 

    gTriageDumpDataArray =
        (PKTRIAGE_DUMP_DATA_ARRAY) ((PUCHAR) pBuffer + sizeof(KBUGCHECK_REASON_CALLBACK_RECORD));

    // 
    // Initialize the dump data block array. 
    // 
 
    Status = KeInitializeTriageDumpDataArray(gTriageDumpDataArray, ARRAY_SIZE);
    if (!NT_SUCCESS(Status)) {
        ExFreePoolWithTag(pBuffer, 'Xmpl');
        gTriageDumpDataArray = NULL;
        gBugcheckTriageCallbackRecord = NULL;
        return Status;
    }

    //
    // Set up a callback record
    //    

    bSuccess = KeRegisterBugCheckReasonCallback(gBugcheckTriageCallbackRecord, 
                                                ExampleBugCheckCallbackRoutine, 
                                                KbCallbackTriageDumpData, 
                                                (PUCHAR)"Example"); 

    if ( !bSuccess ) {
         ExFreePoolWithTag(gTriageDumpDataArray, 'Xmpl');
         gTriageDumpDataArray = NULL;
         return STATUS_UNSUCCESSFUL;
    }

    //
    // It is possible to add a range to the array before bugcheck if it is
    // guaranteed to remain valid for the lifetime of the driver.
    // The value could change before bug check, but the address and size
    // must remain valid.
    //

    KeAddTriageDumpDataBlock(gTriageDumpDataArray, &gDriverData1, sizeof(gDriverData1));

    //
    // For an example, allocate another buffer here for later addition tp the array.
    //

    gpDriverData2 = ExAllocatePoolWithTag(NonPagedPoolNx, sizeof(ULONG64), 'Xmpl');
    if (gpDriverData2 != NULL) {
        *gpDriverData2 = 0xBBBBBBBB;
    }

    return STATUS_SUCCESS;
} 



// Deregister function

VOID CleanupTriageDataCallbacks() 
{ 

    //
    // Call this routine from DriverUnload
    //

    if (gBugcheckTriageCallbackRecord != NULL) {
        KeDeregisterBugCheckReasonCallback( gBugcheckTriageCallbackRecord );
        ExFreePoolWithTag( gBugcheckTriageCallbackRecord, 'Xmpl' );
        gTriageDumpDataArray = NULL;
    }

}

只有非分页内核模式地址应与此回调方法一起使用。

验证是否设置了KB_TRIAGE_DUMP_DATA_FLAG_BUGCHECK_ACTIVE标志后,只能从 KbCallbackTriageDumpData 例程使用 MmIsAddressValid 函数。 当前始终需要设置此标志,但如果未设置其他同步,则调用例程是不安全的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/597551.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-13-按键实验

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

FTP协议与工作原理

一、FTP协议 FTP&#xff08;FileTransferProtocol&#xff09;文件传输协议&#xff1a;用于Internet上的控制文件的双向传输&#xff0c;是一个应用程序&#xff08;Application&#xff09;。基于不同的操作系统有不同的FTP应用程序&#xff0c;而所有这些应用程序都遵守同…

计算机网络【应用层】邮件和DNS

文章目录 电子邮件DNSDNS提供的服务&#xff1a;域名分级域名解析流程DNS资源记录DNS服务器类型 电子邮件 使用SMTP协议发送邮件之前&#xff0c;需要将二进制多媒体数据编码为ASCII码SMTP一般不使用中间邮件服务器发送邮件&#xff0c;如果收件服务器没开机&#xff0c;那么会…

解决jar包中没有主清单目录的问题

文章目录 解决jar包中没有主清单目录的问题问题描述环境描述方法一 | 阿里巴巴构造器的通用解决方案方式二 | 指定MANIFEST.MF路径 解决jar包中没有主清单目录的问题 问题描述 很简单可能很多人都遇到过&#xff0c;maven项目打成jar包后执行报错&#xff1a;jar包中没有主清单…

在模方中已经选好水岸线了,但是点处理瓦块的时候还是提示水岸线没选

答&#xff1a;能部分位置不闭合&#xff0c;双击右键闭合一下&#xff0c;可以强行闭合缺口。 模方是一款针对实景三维模型的冗余碎片、水面残缺、道路不平、标牌破损、纹理拉伸模糊等共性问题研发的实景三维模型修复编辑软件。模方4.1新增自动单体化建模功能&#xff0c;支持…

高情商回复(不是)

背景介绍 在抖音上有这样的视频&#xff0c;视频就是一张图&#xff0c;图上问了一个问题&#xff1a;饭局上&#xff0c;你去帮领导盛饭&#xff0c;领导接过后说&#xff1a;‘盛这么多&#xff0c;喂猪呢&#xff1f;’咋回&#xff1f; 底下有一个搞笑评论&#xff1a;猪可…

迅雷永久破解

链接&#xff1a;https://pan.baidu.com/s/1ZGb1ljTPPG3NFsI8ghhWbA?pwdok7s 下载后解压 以管理员身份运行绿化.bat&#xff0c;会自动生成快捷方式&#xff0c;如果没有可以在program中运行Thunder.exe

UDP如何端口映射?

UDP端口映射是一种网络技术&#xff0c;通过它可以实现在异地组网的情况下&#xff0c;不暴露在公网上&#xff0c;通过私有通道传输数据&#xff0c;并对数据进行安全加密&#xff0c;以保障数据的安全性。这项技术在如今日益复杂和危险的网络环境中显得尤为重要。 UDP&#x…

Rust 适合哪些场景?

目录 二、Rust 适合哪些场景&#xff1f; 三、Rust 社区的发展趋势如何&#xff1f; 四、Rust 快速搭建一个WebServer服务器 一、Rust是什么&#xff1f; Rust是一门赋予每个人构建可靠且高效软件能力的语言。 Rust 程序设计语言 一门帮助每个人构建可靠且高效软件的语言。…

tomcat-以服务的方式重启tomcat

背景 双击tomcat的bin目录下面的startup.bat&#xff0c;会留下一个cmd的窗口&#xff0c;很不优雅 使用service服务的方式启动&#xff0c;并且设置为自动启动 找到tomcat的bin目录输入cmd&#xff0c;按Enter&#xff0c;进入命令行界面。执行“service.bat install” 。&…

详解嵌入式MCU运行时分配的stack和heap

目录 概述 1 认识stack和heap 1.1 栈区&#xff08;stack&#xff09; 1.2 堆区&#xff08;heap&#xff09; 2 stack和heap的区别 2.1 管理方式的不同 2.2 空间大小不同 2.3 产生碎片不同 2.4 增长方式不同 2.5 分配方式不同 2.6 分配效率不同 3 确定stack和heap…

架构师:搭建Spring Security、OAuth2和JWT 的安全认证框架

1、简述 Spring Security 是 Spring 生态系统中的一个强大的安全框架,用于实现身份验证和授权。结合 OAuth2 和 JWT 技术,可以构建一个安全可靠的认证体系,本文将介绍如何在 Spring Boot 中配置并使用这三种技术实现安全认证,并分析它们的优点。 2、Spring Security Spri…

Linux基础04-Linux中目录和文件都能操作的命令

前面两节我们分别学习了目录操作命令和文件操作命令&#xff0c;那么有没有一些既可以操作目录&#xff0c;又可以操作文件的命令呢&#xff1f; 这样我们就不需要记住两套命令了。 其实还真有&#xff0c;今天这一章就带大家学习Linux中目录和文件都能操作的命令 最近无意间获…

深度学习之DCGAN

目录 须知 转置卷积 DCGAN 什么是DCGAN 生成器代码 判别器代码 补充知识 LeakyReLU&#xff08;x&#xff09; torch.nn.Dropout torch.nn.Dropout2d DCGAN完整代码 运行结果 图形显示 须知 在讲解DCGAN之前我们首先要了解转置卷积和GAN 关于GAN在这片博客中已经很…

GraphGPT——图结构数据的新语言模型

在人工智能的浪潮中&#xff0c;图神经网络&#xff08;GNNs&#xff09;已经成为理解和分析图结构数据的强大工具。然而&#xff0c;GNNs在面对未标记数据时&#xff0c;其泛化能力往往受限。为了突破这一局限&#xff0c;研究者们提出了GraphGPT&#xff0c;这是一种为大语言…

ASP.NET MVC(二) HtmlHelper

强类型 》》》 Form Html.Action() 执行一个Action&#xff0c;并返回html字符串。 Html.ActionLink() 生成一个超链接。 》》》 htmlhelper 扩展方法 /// 扩展方法 三要素 静态类静态方法this 》》》》上面需要引入命名空间&#xff0c; 》》》 不需要引入命名空间 pu…

每日OJ题_DFS解决FloodFill⑥_力扣529. 扫雷游戏

目录 力扣529. 扫雷游戏 解析代码 力扣529. 扫雷游戏 529. 扫雷游戏 难度 中等 让我们一起来玩扫雷游戏&#xff01; 给你一个大小为 m x n 二维字符矩阵 board &#xff0c;表示扫雷游戏的盘面&#xff0c;其中&#xff1a; M 代表一个 未挖出的 地雷&#xff0c;E 代表…

计算机系列之数据库技术

13、数据库技术&#xff08;重点、考点&#xff09; 1、三级模式-两级映像&#xff08;考点&#xff09; 内模式&#xff1a;管理如何存储物理的数据&#xff0c;对应具体物理存储文件。 **模式&#xff1a;**又称为概念模式&#xff0c;就是我们通常使用的基本表&#xff0c…

AquiSense实现UV-C发光二极管里程碑

国际空间站饮水机上使用的UV-C LED技术 紫外线LED水消毒系统制造商AquiSense Technologies宣布&#xff0c;该公司的UV-C LED技术已成功集成到美国国家航空航天局&#xff08;NASA&#xff09;国际空间站&#xff08;ISS&#xff09;上的饮用水分配器中&#xff0c;并自2023年8…

【Git】回滚旧提交版本且不影响最新提交版本

【Git】回滚旧提交版本且不影响最新提交版本 一、场景假设 远程仓库origin中有一个分支main&#xff0c;有4次提交记录&#xff1a;v1、v2、v3、v4。 二、需求 需要回滚旧提交版本&#xff0c;但不影响已有的所有提交版本&#xff08;即不影响最新提交版本&#xff09;&…
最新文章