Windows NT 驱动程序的编译、安装、调试

news2024/11/27 18:49:53

Windows NT 驱动程序的编译、安装、调试

  • Windows NT 驱动介绍
  • NT 驱动代码分析
  • 编译
  • 安装
    • 修改注册表进行安装
    • 使用工具 `DriverMonitor` 进行打开加载安装
  • 调试


Windows NT 驱动介绍

Windows 驱动分为两类,一类是从 Windows NT 遗留下来的驱动模型称为传统的 Windows NT 驱动程序模型,另一类是 Windows 添加了电源管理后的 KMDF (WDM)驱动程序。本文这里首先以最简单的 Windows NT 驱动模型为例介绍 Windows 驱动的简单编写、编译、安装及调试。

NT 驱动代码分析

如果有学习过 Linux 驱动的同学,基本上会了解到驱动的编写其实都是内核规定好的 API 需要去声明和定义的,根据一定的驱动模型或者范例来进行编写即可实现自己定义的驱动功能。(可参考 Linux 驱动:Linux驱动开发——(Linux内核GPIO操作库函数)gpio(1))。

在 Linux 驱动中内核规定要添加驱动,需要自己实现驱动的入口函数和退出函数,然后用如下的宏定义进行声明:

module_init(led_init);	//驱动入口函数声明
module_exit(led_exit);	//驱动退出函数声明
MODULE_LICENSE("GPL");	//驱动遵循的开源规则声明

而在 Windows 驱动中也有类似的模块入口函数和模块退出函数。区别是在 Windows 驱动中函数入口都统一由一个固定的函数名称 DriverEntry,你只需要在自己的驱动代码中实现这个固定的函数入口(Windows 内核会在加载驱动后主动调用这个入口函数进行驱动的初始化操作),该函数的具体声明如下:

NTSTATUS DriverEntry(
  _In_ PDRIVER_OBJECT  DriverObject,
  _In_ PUNICODE_STRING RegistryPath
);
  • 参数

    • DriverObject [in]:指向 DRIVER_OBJECT 结构的指针,该结构表示驱动程序的 WDM 驱动程序对象。
    • RegistryPath [in]:指向 UNICODE_STRING 结构的指针,该结构指定注册表中驱动程序 的 Parameters 键 的路径。
  • 返回值
    如果例程成功,则必须返回STATUS_SUCCESS。 否则,它必须返回 ntstatus.h 中定义的错误状态值之一。

Windows NT 模型驱动和 WDM 模型驱动都是由 DriverEntry 入口函数开始加载驱动和完成驱动最初的初始化操作的(这也是 Windows 内核固定会调用的驱动入口函数)。所以我们编写 Windows 驱动其实也是从最初的 DriverEntry 入口函数开始。

这里首先编写一个最简单的 Windows NT 驱动作为模型例程进行讲解,整个驱动源文件有两个文件组成:驱动源文件(helloNt.cpp)和驱动头文件(helloNt.h)。

这里最好参考《Windows 驱动开发环境搭建》这篇文章的内容已经搭建好了 Windows 驱动开发环境,并且使用 VS2019 创建 KMDF 驱动解决方案项目,如下图:

在这里插入图片描述
在这里插入图片描述

使用 VS2019 创建完成解决方案后会自动创建如下文件,这里由于我们想要创建一个最简单的 Windows NT 驱动,所以删除不需要的文件即可(Device.c、Device.h、Queue.c、Queue.h、helloNt.inf)。

在这里插入图片描述
删除完成后只保留 Driver.c 和 Driver.h

在这里插入图片描述

这里需要注意,将项目属性进行一些设置(平台参数 x64、编译将警告视为错误设置为否、链接将警告视为错误设置为否、取消 WPP trace 运行)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

驱动模块具体代码内容如下:

Driver.c

#include "driver.h"


#ifdef ALLOC_PRAGMA
#pragma alloc_text (INIT, CreateDevice)
#pragma alloc_text (PAGE, HelloDDKDispatchRoutine)
#pragma alloc_text (PAGE, HelloDDKUnload)
#endif

NTSTATUS
DriverEntry(
    _In_ PDRIVER_OBJECT  DriverObject,
    _In_ PUNICODE_STRING RegistryPath
    )
/*++

Routine Description:
    DriverEntry initializes the driver and is the first routine called by the
    system after the driver is loaded. DriverEntry specifies the other entry
    points in the function driver, such as EvtDevice and DriverUnload.

Parameters Description:

    DriverObject - represents the instance of the function driver that is loaded
    into memory. DriverEntry must initialize members of DriverObject before it
    returns to the caller. DriverObject is allocated by the system before the
    driver is loaded, and it is released by the system after the system unloads
    the function driver from memory.

    RegistryPath - represents the driver specific path in the Registry.
    The function driver can use the path to store driver related data between
    reboots. The path does not store hardware instance specific data.

Return Value:

    STATUS_SUCCESS if successful,
    STATUS_UNSUCCESSFUL otherwise.

--*/
{
    NTSTATUS status;
    KdPrint(("Enter DriverEntry\n"));

    //注册其他驱动调用函数入口
    DriverObject->DriverUnload = HelloDDKUnload;
    DriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutine;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutine;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutine;
    DriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutine;

    //创建驱动设备对象
    status = CreateDevice(DriverObject);

    KdPrint(("DriverEntry end\n"));
    return status;
}




/************************************************************************
* 函数名称:CreateDevice
* 功能描述:初始化设备对象
* 参数列表:
      pDriverObject:从I/O管理器中传进来的驱动对象
* 返回 值:返回初始化状态
*************************************************************************/
#pragma INITCODE
NTSTATUS CreateDevice(
    IN PDRIVER_OBJECT	pDriverObject)
{
    NTSTATUS status;
    PDEVICE_OBJECT pDevObj;
    PDEVICE_EXTENSION pDevExt;

    //创建设备名称
    UNICODE_STRING devName;
    RtlInitUnicodeString(&devName, L"\\Device\\MyDDKDevice");

    //创建设备
    status = IoCreateDevice(pDriverObject,
        sizeof(DEVICE_EXTENSION),
        &devName,
        FILE_DEVICE_UNKNOWN,
        0, TRUE,
        &pDevObj);
    if (!NT_SUCCESS(status))
        return status;

    pDevObj->Flags |= DO_BUFFERED_IO;
    pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
    pDevExt->pDevice = pDevObj;
    pDevExt->ustrDeviceName = devName;
    //创建符号链接
    UNICODE_STRING symLinkName;
    RtlInitUnicodeString(&symLinkName, L"\\??\\HelloDDK");
    pDevExt->ustrSymLinkName = symLinkName;
    status = IoCreateSymbolicLink(&symLinkName, &devName);
    if (!NT_SUCCESS(status))
    {
        IoDeleteDevice(pDevObj);
        return status;
    }
    return STATUS_SUCCESS;
}

/************************************************************************
* 函数名称:HelloDDKUnload
* 功能描述:负责驱动程序的卸载操作
* 参数列表:
      pDriverObject:驱动对象
* 返回 值:返回状态
*************************************************************************/
#pragma PAGEDCODE
VOID HelloDDKUnload(IN PDRIVER_OBJECT pDriverObject)
{
    PDEVICE_OBJECT	pNextObj;
    KdPrint(("Enter DriverUnload\n"));
    pNextObj = pDriverObject->DeviceObject;
    while (pNextObj != NULL)
    {
        PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
            pNextObj->DeviceExtension;

        //删除符号链接
        UNICODE_STRING pLinkName = pDevExt->ustrSymLinkName;
        IoDeleteSymbolicLink(&pLinkName);
        pNextObj = pNextObj->NextDevice;
        IoDeleteDevice(pDevExt->pDevice);
    }
}

/************************************************************************
* 函数名称:HelloDDKDispatchRoutine
* 功能描述:对读IRP进行处理
* 参数列表:
      pDevObj:功能设备对象
      pIrp:从IO请求包
* 返回 值:返回状态
*************************************************************************/
#pragma PAGEDCODE
NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj,
    IN PIRP pIrp)
{
    KdPrint(("Enter HelloDDKDispatchRoutine\n"));
    NTSTATUS status = STATUS_SUCCESS;
    // 完成IRP
    pIrp->IoStatus.Status = status;
    pIrp->IoStatus.Information = 0;	// bytes xfered
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);
    KdPrint(("Leave HelloDDKDispatchRoutine\n"));
    return status;
}

Driver.h

#pragma once

#ifdef __cplusplus
extern "C"
{
#endif

#include <ntddk.h>
#include <NTDDK.h>

#ifdef __cplusplus
}
#endif 

#define arraysize(p) (sizeof(p)/sizeof((p)[0]))

typedef struct _DEVICE_EXTENSION {
	PDEVICE_OBJECT pDevice;
	UNICODE_STRING ustrDeviceName;
	UNICODE_STRING ustrSymLinkName;
} DEVICE_EXTENSION, * PDEVICE_EXTENSION;

NTSTATUS CreateDevice(IN PDRIVER_OBJECT pDriverObject);
VOID HelloDDKUnload(IN PDRIVER_OBJECT pDriverObject);
NTSTATUS HelloDDKDispatchRoutine(IN PDEVICE_OBJECT pDevObj,
	IN PIRP pIrp);


编译

这里在编译前,首先已经根据文章《Windows 驱动开发环境搭建》完成了基本的编译环境搭建。所以我们直接在 VS 2019 中将解决方案进行构建即可。

前面我们已经对项目的属性进行了设置,这里只要选取我们设置的平台 debug-X64,然后点击“生成”——“生成解决方案”/“重新生成解决方案”即可生成我们需要的驱动文件。

在这里插入图片描述

在这里插入图片描述

安装

对于我们自己编写的驱动程序,为了防止驱动程序的加载使用导致我们的操作系统崩溃(Windows 驱动程序加载后就会运行在 RING-0 内核空间中,这个空间中的程序崩溃会直接导致 Windows 操作系统崩溃,最常见的现象就是蓝屏)。所以我们需要搭建 Windows 虚拟机环境,然后在虚拟机中进行新驱动的安装调试。

由于 Windows NT 驱动程序实际上就是 Windows 服务驱动程序,我们这里编写的并没有真正与设备进行相关,所以你可以理解它是一个运行在 Windows 内核空间的服务程序。对于 Windows NT 驱动程序的安装,这里可以给出两种方式:

  • 通过工具 DriverMonitor 进行打开加载安装
  • 修改注册表,添加驱动服务,重启计算机使用 net start helloNt 命令进行加载启动

修改注册表进行安装

使用快捷键 Win+R 打开对话框后输入 regedit 运行后打开系统的注册表。在注册表中我们找到服务驱动程序加载的注册位置。

计算机\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\helloNt

在其中添加一个新的服务程序项目,然后加入一下内容进行描述,注意其中的 ImagePath 描述内容为加载的驱动程序文件(helloNt.sys)。

在这里插入图片描述
其中涉及到驱动文件的路径,输入内容如下:
在这里插入图片描述
\??\C:\Tools\helloNt\helloNt\helloNt.sys

驱动文件存放位置:C:\Tools\helloNt\helloNt
在这里插入图片描述

注册表内容都添加配置完成后,我们重启计算机(重启才会使得注册表生效)。
重启完成后,使用快捷键 Win+X,选择 Windows powershell(管理员)打开终端对话框,使用命令 net start helloNt 启动加载驱动程序。

在这里插入图片描述
这时候驱动程序就已经加载启动了。我们可以打开“系统信息”工具来查看这个服务的状态。

在这里插入图片描述
可以看到这里已经启动了这个驱动程序。

使用工具 DriverMonitor 进行打开加载安装

本来这个 DriverMonitor 工具是 DriverStudio 工具集的一部分,但是这个 DriverStudio 工具集也已经被淘汰了,所以目前其实这个并不好找。我在 github 上找到一个仓库中存放有这部分的工具,所以可以从这里使用 git clone 进行下载使用。(https://github.com/HotIce0/windows_kernel_driver_learn_note.git)

在这里插入图片描述

将这个工具下载下来解压后,打开可以看到一个简单的界面:

在这里插入图片描述
选择“File”,“Open Driver” 打开驱动文件。

在这里插入图片描述
在这里插入图片描述

然后选择“File”-“Start Driver”,即可完成驱动的加载和运行。

在这里插入图片描述

调试

这里的例程其实是从书籍《Windows 驱动开发技术详解》(张帆)中得到的,驱动加载的时候都是正常的,但是我们如果使用手动卸载启动时就会出现问题,使用 net 命令卸载驱动:

net stop helloNt

结果发现系统蓝屏了(Windows 崩溃了)

在这里插入图片描述
这时候就需要对驱动进行简单调试来找到出问题的地方。虽然我们在驱动中添加了 log 打印(KdPrint),但是当系统崩溃后,我们并不能及时看到 log 的打印输出(已经崩溃了还看个锤子,只能看到蓝屏)。针对这种情况就需要我们使用 windbg 双机调试来进行了。(双机调试环境请参考文章《windbg 双机调试环境搭建(虚拟机)》)

在宿主机上启动 windbg 后,与虚拟机建立连接。执行 windbg 传统调试步骤:

  1. 加载符号表(尤其是自己编译的驱动程序的符号表)
  2. 设置驱动模块入口断点,或者直接操作使系统崩溃(崩溃会自动停止在断点位置)
  3. 查看当前调用堆栈,参考源代码推断崩溃原因
  4. 修改源码,重新编译,加载验证

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
这里首先 break 下来,然后通过命令加载符号表,或者使用 GUI 操作加载符号表。

.srcpath+ "C:\Tools\helloNt\*.pdb"
在这里插入图片描述

在这里插入图片描述
由于是卸载操作引起的崩溃,所以我们在卸载函数这里添加断点:

在这里插入图片描述
执行 net stop helloNt 命令后,windbg 会自动跳转到断点(记载了符号表)也会显示对应的代码。

在这里插入图片描述

单步执行,然后发现崩溃点的调用栈:
在这里插入图片描述
在这里插入图片描述
这里发现是在调用 Unload() 中的 DeleteSymbolicLink 的时候出现的问题。回顾我们这里的代码:

VOID HelloDDKUnload(IN PDRIVER_OBJECT pDriverObject)
{
    PDEVICE_OBJECT	pNextObj;
    KdPrint(("Enter DriverUnload\n"));
    pNextObj = pDriverObject->DeviceObject;
    while (pNextObj != NULL)
    {
        PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
            pNextObj->DeviceExtension;
        UNICODE_STRING pLinkName = pDevExt->ustrSymLinkName;
        IoDeleteSymbolicLink(&pLinkName);
        pNextObj = pNextObj->NextDevice;
        IoDeleteDevice(pDevExt->pDevice);
    }
}

发现这里调用了 IoDeleteSymbolicLink(&pLinkName);,然后我们在这里打印 pLinkName 的地址看一下这个地址空间是否正常可访问。

也可以使用 !analyze -v 进行分析(这个分析结果可供参考)
在这里插入图片描述

发现结果和我们预测的位置基本一致。那问题就出现在这个链接变量这里了。为什么我们卸载链接变量会崩溃呢?

在这里插入图片描述
通过 Unload 局部变量中的数据可以看到这里的 pDevExt->ustrSymLinkName; 内存是不可以访问的,也就是说在这里的时候已经出现问题了。回到源代码中查看初始化部分发现这个字符串变量是在 CreateDevice 的时候放进去的:

	//创建设备名称
	UNICODE_STRING devName;
	RtlInitUnicodeString(&devName,L"\\Device\\MyDDKDevice");
	
	//创建设备
	status = IoCreateDevice( pDriverObject,
						sizeof(DEVICE_EXTENSION),
						&(UNICODE_STRING)devName,
						FILE_DEVICE_UNKNOWN,
						0, TRUE,
						&pDevObj );
	if (!NT_SUCCESS(status))
		return status;

	pDevObj->Flags |= DO_BUFFERED_IO;
	pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
	pDevExt->pDevice = pDevObj;
	pDevExt->ustrDeviceName = devName;

这里创建了一个字符串变量 devName,然后将这个变量放到了 pDevExt 中。但是这里发现这个 devName 是这个函数的局部变量,这里放进去的只是字符串指针,而在后面的函数调用中取出了已经释放空间的指针进行使用肯定会有问题。

找到问题后,我们修改一下,把这里的局部变量修改成全局变量或者静态变量试一下:


UNICODE_STRING gdevName;
UNICODE_STRING gsymLinkName;

/************************************************************************
* 函数名称:CreateDevice
* 功能描述:初始化设备对象
* 参数列表:
      pDriverObject:从I/O管理器中传进来的驱动对象
* 返回 值:返回初始化状态
*************************************************************************/
#pragma INITCODE
NTSTATUS CreateDevice(
    IN PDRIVER_OBJECT	pDriverObject)
{
    NTSTATUS status;
    PDEVICE_OBJECT pDevObj;
    PDEVICE_EXTENSION pDevExt;

    //创建设备名称
    //UNICODE_STRING devName;
    RtlInitUnicodeString(&gdevName, L"\\Device\\MyDDKDevice");

    //创建设备
    status = IoCreateDevice(pDriverObject,
        sizeof(DEVICE_EXTENSION),
        &gdevName,
        FILE_DEVICE_UNKNOWN,
        0, TRUE,
        &pDevObj);
    if (!NT_SUCCESS(status))
        return status;

    pDevObj->Flags |= DO_BUFFERED_IO;
    pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
    pDevExt->pDevice = pDevObj;
    pDevExt->ustrDeviceName = gdevName;
    //创建符号链接
    //UNICODE_STRING symLinkName;
    RtlInitUnicodeString(&gsymLinkName, L"\\??\\HelloDDK");
    pDevExt->ustrSymLinkName = gsymLinkName;
    status = IoCreateSymbolicLink(&gsymLinkName, &gdevName);
    if (!NT_SUCCESS(status))
    {
        IoDeleteDevice(pDevObj);
        return status;
    }
    return STATUS_SUCCESS;
}

发现修改成全局变量后,结果还是一样的,这里的 memory 还是无法正常 read。

在这里插入图片描述

在这里插入图片描述

比较发现这里是由于这部分的 buffer 空间 read error,修改代码,将这部分的字符串初始化从这个 #pragma INITCODE 声明的 CreateDevice 函数中移到 DriverEntry 中:


NTSTATUS
DriverEntry(
    _In_ PDRIVER_OBJECT  DriverObject,
    _In_ PUNICODE_STRING RegistryPath
    )
{
    NTSTATUS status;
    KdPrint(("Enter DriverEntry\n"));

    UNICODE_STRING devName;
    UNICODE_STRING symLinkName;
    RtlInitUnicodeString(&devName, L"\\Device\\MyDDKDevice");
    RtlInitUnicodeString(&symLinkName, L"\\??\\HelloDDK");

    //注册其他驱动调用函数入口
    DriverObject->DriverUnload = HelloDDKUnload;
    DriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKDispatchRoutine;
    DriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloDDKDispatchRoutine;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = HelloDDKDispatchRoutine;
    DriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKDispatchRoutine;

    //创建驱动设备对象
    status = CreateDevice(DriverObject, devName, symLinkName);

    KdPrint(("DriverEntry end\n"));
    return status;
}

/************************************************************************
* 函数名称:CreateDevice
* 功能描述:初始化设备对象
* 参数列表:
      pDriverObject:从I/O管理器中传进来的驱动对象
* 返回 值:返回初始化状态
*************************************************************************/
#pragma INITCODE
NTSTATUS CreateDevice(
    IN PDRIVER_OBJECT	pDriverObject,
    IN UNICODE_STRING  devName,
    IN UNICODE_STRING  symLinkName)
{
    NTSTATUS status;
    PDEVICE_OBJECT pDevObj;
    PDEVICE_EXTENSION pDevExt;

    //创建设备名称
    //UNICODE_STRING devName;
    //RtlInitUnicodeString(&devName, L"\\Device\\MyDDKDevice");

    //创建设备
    status = IoCreateDevice(pDriverObject,
        sizeof(DEVICE_EXTENSION),
        &devName,
        FILE_DEVICE_UNKNOWN,
        0, TRUE,
        &pDevObj);
    if (!NT_SUCCESS(status))
        return status;

    pDevObj->Flags |= DO_BUFFERED_IO;
    pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
    pDevExt->pDevice = pDevObj;
    pDevExt->ustrDeviceName = devName;
    //创建符号链接
    //UNICODE_STRING symLinkName;
    //RtlInitUnicodeString(&symLinkName, L"\\??\\HelloDDK");
    pDevExt->ustrSymLinkName = symLinkName;
    status = IoCreateSymbolicLink(&symLinkName, &devName);
    if (!NT_SUCCESS(status))
    {
        IoDeleteDevice(pDevObj);
        return status;
    }
    return STATUS_SUCCESS;
}

在这里插入图片描述
并且可以看到加载和卸载驱动操作已经正常了。说明我们的判断是正确的。

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1、Winform项目创建

项目创建的过程时比较简单的,要点在于选择基本库。 这里我们选择.Netframework 4.7.2,为什么使用这个呢?因为这个大多数windows系统上都装的有。如果使用.Net Core什么的,可能还需要再下载对应的运行库,影响用户体验。 具体步骤如下: 选择创建新项目 选择创建桌面应用…

RabbitMQ如何保证消息的可靠性6000字详解

RabbitMQ通过生产者、消费者以及MQ Broker达到了解耦的特点&#xff0c;实现了异步通讯等一些优点&#xff0c;但是在消息的传递中引入了MQ Broker必然会带来一些其他问题&#xff0c;比如如何保证消息在传输过程中可靠性&#xff08;即不让数据丢失&#xff0c;发送一次消息就…

GDB调试基础知识

文章目录 概念准备工作常用命令说明启动与退出给程序设置参数/获取设置参数GDB使用帮助查看当前文件代码查看非当前文件代码查看及设置显示的行数断点操作调试操作 概念 GDB 是由 GNU 软件系统社区提供的调试工具&#xff0c;同 GCC 配套组成了一套完整的开发环境&#xff0c;…

Python基础教程——60个基础练习(三)

41-字符串格式化 "%s is %s years old" % (bob, 23) # 常用 "%s is %d years old" % (bob, 23) # 常用 "%s is %d years old" % (bob, 23.5) # %d 是整数 常用 "%s is %f years old" % (bob, 23.5) "%s is %5.2f…

ListBox基本用法

作用&#xff1a;列表框&#xff0c;用于以列表的形式展示数据。 常用属性&#xff1a; 允许多列显示数据 添加数据项集合 常用事件&#xff1a; 选择项变化时触发该事件 后台代码示范&#xff1a; //列表框项目选择变化时被触发private void listBox1_SelectedIndexChanged…

Flutter 跳转应用市场评分——超简洁实现

最近在做flutter跳转去应用市场评分的功能&#xff0c;虽然是一个很小的功能&#xff0c;但是要做的既简单又高效&#xff0c;同时又能把细节考虑到&#xff0c;还是有坑要走的&#xff0c;这边记录一下。 背景 做应用市场相关的运营&#xff0c;在app内增加评分引导&#xf…

经典目标检测R-CNN系列(1)开山之作R-CNN

经典目标检测R-CNN系列(1)开山之作R-CNN 2014年&#xff0c;大神RBG&#xff08;Ross Girshick&#xff09;等人将卷积神经网络&#xff08;Convolutional Neural Network&#xff0c;CNN&#xff09;应用于目标检测任务中&#xff0c;在PASCAL VOC 2012数据集上&#xff0c;能…

vue 如何发布并部署到服务器

一般情况npm run build即可 从而生成vue代码直接放到服务器即可 这里的具体情况要看package.json里面的配置从而使用命令 会生成dist就是该项目的发布包

软件测试项目经验重要吗?

目前从行业薪资排名看&#xff0c;IT行业是我们普通人能够接触到的高薪行业&#xff0c;像金融、银行和投行等高薪职位&#xff0c;张雪峰老师在他的视频中分析过&#xff0c;不是一般人可以拿捏的。IT行业的大部分岗位需要专业的技能&#xff0c;留给我们这些非计算机专业科班…

实现微信机器人开发,个微api

首先微信聊天机器人&#xff0c;是一种通过自然语言模拟人类进行对话的程序。通常运行在特定的软件平台上&#xff0c;如PC平台或者移动终端设备平台。 有兴趣的可以去进行测试&#xff08;E云管家&#xff09;&#xff0c;功能十分全面 文档测试过程中实现多项功能进行管理 …

数据结构--线性表的链式存储结构

这里写目录标题 链式存储结构链表简介格式分类头结点位置示意图与不带头结点的区别 链表的特点 单链表定义链表的代码实现简介实操 基本操作的实现初始化单链表销毁单链表清空单链表求单链表表长 二级目录二级目录二级目录二级目录二级目录二级目录 链式存储结构 链表 简介 格…

QML学习day1

QML学习day1 main.qml import QtQuick 2.15 import QtQuick.Window 2.15 import QtQuick.Controls 2.5Window {width: 640height: 480visible: truecolor:"blue"title: qsTr("Hello World")Button {//按钮id: btn1width: 50height: 50focus: true //聚焦…

P106-100组A卡(R5 240)指南

P106-100组A卡&#xff08;R5 240&#xff09;指南 不建议小白尝试 不建议小白尝试 不建议小白尝试文章目录 P106-100组A卡&#xff08;R5 240&#xff09;指南资料合集硬件软件基础卸载所有原驱动安装驱动修改注册表自动调用——只改一个注册表手动调用——改两个注册表 劝退…

软件设计原则

在软件开发中&#xff0c;为了提高软件系统的可维护性和可复用性&#xff0c;增加软件的可扩展性和灵活性&#xff0c;程序员要尽量根据6条原则来开发程序&#xff0c;从而提高软件开发效率、节约软件开发成本和维护成本。 开闭原则 对扩展开放&#xff0c;对修改关闭。在程序…

Leecode402:移掉 K 位数字

这道题一看想的是可能用回溯或者什么别的方法&#xff0c;但是那样的话时间复杂度非常高&#xff0c;而且也不适用于动态规划&#xff0c;所以观察的话&#xff0c;可以知道从前往后判断的话肯定是前面越小越好&#xff0c;所以只需要前面最小&#xff0c;整体就最小。因此从前…

子网掩码详解

1 子网掩码 IP地址是以网络号和主机号来标示网络上的主机的&#xff0c;我们把网络号相同的主机称之为本地网络&#xff0c;网络号不相同的主机称之为远程网络主机&#xff0c;本地网络中的主机可以直接相互通信&#xff1b;远程网络中的主机要相互通信必须通过本地网关&#…

酸蚀刻对钛医药材料纳米形态表面特性及活化能的影响

引言 由于商业纯钛(CP Ti)具有抗腐蚀性&#xff0c;并且具有哦合适的机械性能以及生物相容性&#xff0c;因此&#xff0c;目前一直被用作牙科植入材料。为了在临床手术中获得高水平的成功&#xff0c;CP Ti的表面质量和形貌是影响植入手术结果的最关键因素之一&#xff0c;近…

GPT使用技巧

五大原则 想要让ChatGPT产出有效的回答&#xff0c;需要遵循以下五个原则&#xff1a; 提问清晰&#xff1a; 请尽可能清晰地描述您的问题 简明扼要&#xff1a; 请尽量使用简单的语言和简洁的句子来表达您的问题 确认问题&#xff1a; 请确认您的问题是清晰、明确和完整…

python接口自动化--token登录(详解)

简介 为了验证用户登录情况以及减轻服务器的压力&#xff0c;减少频繁的查询数据库&#xff0c;使服务器更加健壮。有些登录不是用 cookie 来验证的&#xff0c;是用 token 参数来判断是否登录。token 传参有两种一种是放在请求头里&#xff0c;本质上是跟 cookie 是一样的&…

攻不下dfs不参加比赛(十一)

标题 为什么练dfs题目为什么练dfs 相信学过数据结构的朋友都知道dfs(深度优先搜索)是里面相当重要的一种搜索算法,可能直接说大家感受不到有条件的大家可以去看看一些算法比赛。这些比赛中每一届或多或少都会牵扯到dfs,可能提到dfs大家都知道但是我们为了避免眼高手低有的东…