内核第三讲,进入ring0,以及编写第一个内核驱动程序.

       内核第三讲,进入ring0,以及编写第一个内核驱动程序.

PS: 请下配置双机调试,下方有可能用到.如果不配置,则你可以不用调试, 博客连接: http://www.cnblogs.com/iBinary/p/8260969.html

一丶进入ring0之前的简介

进入0环之前,我们要明白操作系统的设计,操作系统允许驱动程序使用In out等等特权指令来操作高2G的内存.那么必然会有接口.

图示如下.

那么我们可以模拟一个驱动程序来进入0环.

而操作系统提供的接口的,有专门的名称. 叫做DDK, 现在改名为WDK了.

DDK: Driver Development Kit  驱动程序开发包,例如我们写的3环系统下,用到的SDK,也可以成为是API.只不过现在叫做内核方法(内核函数)了.

WDK:Windows Driver Kit 

WDK是DDK升级而来的.操作系统为了支持热插拔,所以对DDK升级了.热插拔就是U盘插入系统.不用安装驱动了.和U盘绑定在一起了.

 

WDK官方下载连接: https://developer.microsoft.com/zh-cn/windows/hardware/windows-driver-kit

我自己用的是WDK7.1.7600,会上传到课堂资料中.有兴趣的可以下载.

注意: 如果编写驱动程序,请下载对应系统的WDK,因为驱动程序不兼容.只会跟着系统走.

二丶WDK的安装.

下载之后直接下一步,下一步即可. 如果不会建议百度.

 

 

三丶进入0环,编写第一个内核驱动程序

在进入ring0之前,我们要知道,不管是驱动程序,或者是应用程序,都会有一个入口点.

比如控制台的入口点是 main,窗口的是winmain.那么看下驱动程序的入口点是什么.

PS:安装好WDK之后,会有帮助文档.可以参考帮助文档.

如果我们要编写内核驱动,则要看内核驱动的入口点,也就是kerner mode,内核模型.

如果是编写硬件驱动,则寻找WDM即可.

 

NTSTATUS 
  DriverEntry(         //驱动的入口点
    __in struct _DRIVER_OBJECT  *DriverObject,
    __in PUNICODE_STRING  RegistryPath 
    )
  {...} 

返回值: STATUS_SUCCESS

内核输出的API

ULONG
  DbgPrint(
    IN PCHAR  Format,
    . . . .  [arguments] 
    );

利用入口我们可以简单编写一个内核驱动了.

驱动我们可以用C语言去写.也可以是C++

 

#include <Ntddk.h> //编写内核驱动需要包含NTddk头文件.

NTSTATUS DriverEntry(__in struct _DRIVER_OBJECT  *DriverObject,
                      __in PUNICODE_STRING  RegistryPath)
{
    int i = 0;
    DbgPrint("HelloWorld, %p\r\n",&i);
    
    return STATUS_SUCCESS;
}

在编译驱动程序的是否,我们需要一个sources  文件

格式:

TARGETNAME= MyFirstDrive          //指明编译的文件名 
TARGETTYPE=DRIVER                 //指明编译的类型
SOURCES= MyFirstDrive.c            //指明编译的文件

图示:

 

 编译的时候找我们的开发包.

通过开始程序,找到我们的开发包中的编译XP命令框.点开.

输入编译命令 build 文件名

回车就可以编译我们的驱动程序了.

成功会生成一个sys后缀的文件.那么我们就可使用了.

四丶双机调试,加载我们的驱动.

我们以调试系统启动.然后使用加载驱动的工具,加载我们的驱动,那么就可以在调试器中看到我们的驱动代码了.

当我们启动之后,我们可以查看下调试器,可以调试我们的代码.

请注意我们打印的地址,地址是高2G的空间.所以我们就进入了0环空间了.

五丶编写驱动卸载功能.

我们的驱动现在可以加载.启动.停止.但是不能卸载.原因就是我们没有写卸载的函数.

比如我们的DLL,  DLL来的是否还分为4中情况.

很简单.只需要把我们的驱动代码改变一下即可.

#include <Ntddk.h> //编写内核驱动需要包含NTddk头文件.


//卸载回调函数
VOID Unload(__in struct _DRIVER_OBJECT  *DriverObject)
{
  DbgPrint("Unload MyDrive\n");
}

NTSTATUS DriverEntry(__in struct _DRIVER_OBJECT  *DriverObject,
                      __in PUNICODE_STRING  RegistryPath)
{
    int i = 0;
    DbgPrint("HelloWorld, %p\r\n",&i);
    
    //注册一下驱动卸载的函数
    DriverObject->DriverUnload = Unload;

    return STATUS_SUCCESS;
}

给个卸载的函数指针即可.注意启动入口点的参数是一个结构体.启动你想要支持卸载驱动.那么就写上卸载驱动的函数指针即可.

六丶蓝屏的出现.

编写驱动代码,不像我们编写ring3下的应用程序,崩溃了就是崩溃了. 我们写驱动程序恨不得写一行,检查500行.

看下蓝屏的实现.只要我们的程序异常,那么就会蓝屏.

代码:

#include <Ntddk.h> //编写内核驱动需要包含NTddk头文件.


//卸载回调函数
VOID Unload(__in struct _DRIVER_OBJECT  *DriverObject)
{
  DbgPrint("Unload MyDrive\n");
}

NTSTATUS DriverEntry(__in struct _DRIVER_OBJECT  *DriverObject,
                      __in PUNICODE_STRING  RegistryPath)
{
    int i = 0;
     int *p = NULL;   //异常代码.会造成C05访问异常.
    DbgPrint("HelloWorld, %p\r\n",&i);
    
   
    *p = 1;       //代码会产生异常,系统会蓝屏.

    //注册一下驱动卸载的函数
    DriverObject->DriverUnload = Unload;

    return STATUS_SUCCESS;
}

 

很简单,就加了一行C05访问异常的代码.

重新编译一下.并且驱动加载.

因为家里电脑系统原因,蓝屏了会紧接着重启.所以来不及截图.请大家自己尝试.

课堂代码资料:   

内核工具下载: 链接:https://pan.baidu.com/s/1o9PjpUU 密码:k5sp

课堂驱动代码资料下载: 链接:https://pan.baidu.com/s/1kXiSluv 密码:jnov

 

转载请著名出处,谢谢.

转载于:https://www.cnblogs.com/iBinary/p/8264251.html

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>软件名称:驱动加载工具(InstDrv - 中文版)软件版本:V1.3版软件类型:个人免费版(无插件)运行环境:Windows操作系统开发作者:Coderui更新日期:2008年04月18日联系邮箱:coderui@163.com作者博客:http://hi.baidu.com/coderui>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>-------------------------------------------------介绍:  该软件和网上大家常用的“InstDrv”程序功能几乎一样,现在也支持[命令行控制台]调用了,只是新加入了详细的中文提示信息和拖拽打开文件等,这样可以方便用户的使用。“InstDrv”是一个比较好用的驱动安装、卸载工具。可以帮助您方便的把驱动程序安装上或卸载掉。本程序更多应用于动态调式驱动前的加载操作中。功能:1:执行加载、启动、停止、卸载驱动操作。2:支持文件拖拽打开。3:支持程序窗口总在最前显示。4:操作后有详细的中文提示信息。5:支持[命令行控制台]调用。6:提升程序自身的权限和优先级。说明:压缩包中的“MySYS.sys”文件为测试驱动程序,支持加载、启动、停止、卸载驱动等功能,可以使用“Dbgview”工具查看驱动输出的提示信息。-------------------------------------------------详细的中文提示信息如下:状态:支持拖拽获取文件路径.状态:只能一次拖拽一个文件!状态:拖拽获取文件路径成功!状态:请输入驱动文件的路径名!状态:创建句柄失败!状态:因为不是一个有效文件,所以驱动服务安装失败!状态:因为驱动服务已经存在,所以驱动服务安装失败!状态:因为驱动服务已经启动,所以驱动服务安装失败!状态:驱动服务安装失败!状态:驱动服务安装成功!状态:打开驱动服务失败,可能该驱动服务不存在!状态:驱动服务启动成功!状态:因为驱动服务已经启动,所以驱动服务启动失败!状态:因为不是一个有效驱动,所以驱动服务启动失败!状态:因为驱动服务未被停止,所以驱动服务启动失败!状态:驱动服务启动失败!状态:驱动服务停止成功!状态:因为驱动服务没有启动,所以驱动服务停止失败!状态:驱动服务停止失败!状态:驱动服务卸载成功!状态:因为驱动服务已经启动,所以驱动服务卸载失败!状态:驱动服务卸载失败!状态:提升进程权限失败!状态:提升进程优先级失败!-------------------------------------------------[命令行控制台]调用参数:InstDrv [/i] [/u] [/s] [驱动程序路径名]/i 安装并启动这个驱动程序./u 停止并卸载这个驱动程序./s 不弹出任何消息提示窗口.注意:带有空格的路径,请用双引号把路径括起来.-------------------------------------------------下载说明:如果当前网页连接地址下载不了该软件,您可以发送邮件到coderui@163.com中,向程序作者所要该软件,谢谢您的支持!-------------------------------------------------解压后EXE程序全文MD5校验值为:6f356e16020902a77d57fa44ff21c387专用MD5校验器程序下载地址为:http://hi.baidu.com/coderui/blog/item/e3a1282354c128519822ed24.html>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
<think>我们正在为Intel 82567网络控制器开发Linux内核驱动程序。根据之前提供的文档信息,我们需要参考《Intel® 82566/82567 Gigabit Ethernet Controller Software Developer’s Manual》来了解硬件细节。同时,由于82567通常集成在ICH芯片组中,我们可能还需要参考ICH的Datasheet中关于网络控制器的部分。 开发Linux内核驱动程序的基本步骤包括: 1. 了解硬件规格和寄存器操作 2. 创建驱动程序框架(包括初始化、探测、移除等) 3. 实现网络设备操作的基本函数(打开、关闭、发送、接收等) 4. 处理中断和DMA 5. 注册网络设备 6. 测试和调试 由于82567是一个千兆以太网控制器,Linux内核可能已经提供了支持(例如e1000e驱动),但如果是自定义开发或者需要特定功能,我们可以从零开始或修改现有驱动。 下面将逐步说明如何开发: ### 1. **准备开发环境** - 安装Linux内核头文件或源代码 - 确保有交叉编译工具链(如果目标平台不是开发主机) ### 2. **创建驱动程序的基本结构** Linux内核网络设备驱动使用`struct net_device`结构来表示一个网络设备。我们需要实现该结构所需的操作函数集(`struct net_device_ops`)。 首先,定义一个结构体来保存我们的设备特定信息,通常包括: - 网络设备结构体(`struct net_device *netdev`) - PCI设备结构体(`struct pci_dev *pdev`) - DMA相关资源 - 寄存器基地址 - 自旋锁等同步机制 示例代码: ```c #include <linux/netdevice.h> #include <linux/pci.h> struct my_netdev_priv { struct net_device *netdev; struct pci_dev *pdev; void __iomem *reg_base; // 其他设备特定数据 }; ``` ### 3. **实现PCI探测和移除** 82567通常通过PCI总线连接,因此我们需要注册一个PCI驱动程序。在探测函数中,我们将: - 启用PCI设备 - 请求内存区域 - 映射寄存器空间 - 初始化网络设备结构 - 配置DMA - 注册网络设备 示例代码: ```c static int my_driver_probe(struct pci_dev *pdev, const struct pci_device_id *ent) { struct net_device *netdev; struct my_netdev_priv *priv; int err; // 启用PCI设备 err = pci_enable_device(pdev); if (err) return err; // 请求内存区域 if (pci_request_regions(pdev, DRV_NAME)) { err = -ENODEV; goto fail_disable; } // 映射寄存器空间 priv->reg_base = pci_iomap(pdev, BAR_0, 0); if (!priv->reg_base) { err = -EIO; goto fail_release; } // 分配网络设备结构 netdev = alloc_etherdev(sizeof(struct my_netdev_priv)); if (!netdev) { err = -ENOMEM; goto fail_iounmap; } SET_NETDEV_DEV(netdev, &pdev->dev); pci_set_drvdata(pdev, netdev); priv = netdev_priv(netdev); priv->netdev = netdev; priv->pdev = pdev; // 设置网络设备操作集 netdev->netdev_ops = &my_netdev_ops; // 设置MAC地址(从硬件读取或设置) // ... // 注册网络设备 err = register_netdev(netdev); if (err) goto fail_free_netdev; return 0; fail_free_netdev: free_netdev(netdev); fail_iounmap: pci_iounmap(pdev, priv->reg_base); fail_release: pci_release_regions(pdev); fail_disable: pci_disable_device(pdev); return err; } static void my_driver_remove(struct pci_dev *pdev) { struct net_device *netdev = pci_get_drvdata(pdev); struct my_netdev_priv *priv = netdev_priv(netdev); unregister_netdev(netdev); pci_iounmap(pdev, priv->reg_base); pci_release_regions(pdev); pci_disable_device(pdev); free_netdev(netdev); } static struct pci_device_id my_driver_id_table[] = { { PCI_DEVICE(PCI_VENDOR_ID_INTEL, 0x10f5) }, // 82567的PCI设备ID { 0, } }; MODULE_DEVICE_TABLE(pci, my_driver_id_table); static struct pci_driver my_driver = { .name = DRV_NAME, .id_table = my_driver_id_table, .probe = my_driver_probe, .remove = my_driver_remove, }; module_pci_driver(my_driver); ``` ### 4. **实现网络设备操作** 我们需要实现`struct net_device_ops`中的基本操作函数,至少包括: - `ndo_open`:打开设备(启动传输队列,使能中断等) - `ndo_stop`:关闭设备(停止传输队列,禁用中断等) - `ndo_start_xmit`:发送数据包 - `ndo_set_mac_address`:设置MAC地址 - `ndo_get_stats`:获取网络统计信息 示例: ```c static const struct net_device_ops my_netdev_ops = { .ndo_open = my_netdev_open, .ndo_stop = my_netdev_stop, .ndo_start_xmit = my_netdev_start_xmit, .ndo_set_mac_address = my_netdev_set_mac, .ndo_get_stats = my_netdev_get_stats, }; static int my_netdev_open(struct net_device *netdev) { // 启动设备,分配发送和接收缓冲区,使能中断等 netif_start_queue(netdev); return 0; } static int my_netdev_stop(struct net_device *netdev) { netif_stop_queue(netdev); // 禁用中断,释放缓冲区等 return 0; } static netdev_tx_t my_netdev_start_xmit(struct sk_buff *skb, struct net_device *netdev) { // 将skb中的数据发送到硬件 // 需要设置DMA描述符等 // 如果硬件队列满,则停止传输队列:netif_stop_queue(netdev); // 当有空间时,通过中断或定时器重新启动:netif_wake_queue(netdev); return NETDEV_TX_OK; } ``` ### 5. **处理中断** 在驱动程序中,我们需要注册中断处理函数。在中断处理中,通常处理: - 接收中断:从硬件接收队列中读取数据包,并通过`netif_rx()`或`napi_gro_receive()`传递给上 - 发送中断:当发送完成时,释放发送缓冲区并唤醒传输队列 - 错误中断:处理错误情况 示例: ```c static irqreturn_t my_netdev_interrupt(int irq, void *dev_id) { struct net_device *netdev = dev_id; struct my_netdev_priv *priv = netdev_priv(netdev); u32 status; // 读取中断状态寄存器 status = ioread32(priv->reg_base + INT_STATUS_REG); if (status & RX_INT) { // 处理接收 // 清除中断位 iowrite32(status & RX_INT, priv->reg_base + INT_STATUS_REG); // 接收数据包 my_receive_packets(netdev); } if (status & TX_INT) { // 处理发送完成 iowrite32(status & TX_INT, priv->reg_base + INT_STATUS_REG); netif_wake_queue(netdev); } return IRQ_HANDLED; } // 在open函数中请求中断 static int my_netdev_open(struct net_device *netdev) { // ... request_irq(pdev->irq, my_netdev_interrupt, IRQF_SHARED, netdev->name, netdev); // ... } ``` ### 6. **接收数据包** 在中断处理函数中,当有接收中断时,我们需要从硬件接收队列中读取数据包,并构建`sk_buff`,然后传递给网络协议栈。 示例: ```c static void my_receive_packets(struct net_device *netdev) { struct my_netdev_priv *priv = netdev_priv(netdev); struct sk_buff *skb; // 从硬件接收描述符中获取数据 // 分配sk_buff skb = netdev_alloc_skb(netdev, length); if (!skb) { // 错误处理 return; } // 将数据从DMA缓冲区复制到skb(或者使用零拷贝) memcpy(skb_put(skb, length), dma_buffer, length); skb->protocol = eth_type_trans(skb, netdev); netif_rx(skb); // 更新统计信息 priv->stats.rx_packets++; priv->stats.rx_bytes += length; } ``` ### 7. **调试和日志** 使用`printk`进行内核日志输出,注意使用不同的日志级别(如`KERN_INFO`,`KERN_ERR`)。 ### 8. **编译和加载** 编写Makefile,使用内核构建系统编译驱动,然后使用`insmod`加载。 ### 注意事项 - **同步与并发**:由于中断可能发生在任何时间,需要正确使用锁(如自旋锁)来保护共享数据。 - **DMA操作**:需要正确设置DMA映射,使用`dma_alloc_coherent`或`dma_map_single`等函数。 - **电源管理**:如果需要支持电源管理,需要实现相应的挂起和恢复函数。 - **多队列支持**:现代网卡可能支持多队列,但82567可能只支持单队列。 ### 参考现有驱动 Linux内核中已经存在许多Intel网卡驱动,例如`e1000e`驱动(支持8257x系列)。可以参考`drivers/net/ethernet/intel/e1000e/`下的代码,特别是`netdev.c`和`ich8lan.c`等文件。 ### 相关问题 1. 如何调试Linux内核网络驱动程序? 2. 在82567驱动中如何处理DMA描述符环? 3. 如何为82567网卡实现NAPI?</think>以下是针对Intel 82567网络控制器开发Linux内核驱动程序的详细指南: ### 一、开发准备 1. **硬件文档获取** - 下载《Intel® 82566/82567 Gigabit Ethernet Controller Software Developer’s Manual》[^1] - 重点章节: - 第5章:寄存器映射(Register Maps) - 第6章:DMA引擎配置(DMA Engine Operation) - 第14章:中断机制(Interrupt Handling) - 示例:MAC配置寄存器地址偏移为`0x04200`,需通过`ioread32()`访问 2. **内核开发环境** ```bash # 安装内核头文件和编译工具 sudo apt install linux-headers-$(uname -r) build-essential # 创建驱动目录结构 mkdir -p my_82567_driver/{src,include} ``` --- ### 二、驱动实现步骤 #### 1. PCI设备探测(Probe) ```c #include <linux/pci.h> #include <linux/netdevice.h> #define INTEL_VENDOR_ID 0x8086 #define DEVICE_82567 0x10f5 // 具体ID查手册 static int my_probe(struct pci_dev *pdev, const struct pci_device_id *ent) { struct net_device *netdev; struct my_priv *priv; // PCI设备初始化 pci_enable_device(pdev); pci_request_regions(pdev, "my_82567"); // 映射寄存器空间 void __iomem *ioaddr = pci_iomap(pdev, 0, 0); // 分配网络设备结构 netdev = alloc_etherdev(sizeof(struct my_priv)); priv = netdev_priv(netdev); priv->regs = ioaddr; // 设置MAC地址(从EEPROM读取) eth_hw_addr_set(netdev, (u8 *)&ioaddr[EEPROM_MAC_OFFSET]); // 注册网络设备 register_netdev(netdev); } ``` #### 2. 实现关键操作函数 ```c static const struct net_device_ops my_netdev_ops = { .ndo_open = my_open, .ndo_stop = my_stop, .ndo_start_xmit = my_xmit, .ndo_set_mac_address = my_set_mac, }; static int my_open(struct net_device *netdev) { struct my_priv *priv = netdev_priv(netdev); // 启用DMA引擎 iowrite32(DMA_CTRL_ENABLE, priv->regs + DMA_CTRL_REG); // 请求中断 request_irq(pdev->irq, my_interrupt, IRQF_SHARED, netdev->name, netdev); netif_start_queue(netdev); return 0; } static netdev_tx_t my_xmit(struct sk_buff *skb, struct net_device *netdev) { // 设置发送描述符(零拷贝优化) struct my_tx_desc *txd = &priv->tx_ring[priv->tx_idx]; txd->buffer_addr = dma_map_single(&pdev->dev, skb->data, skb->len, DMA_TO_DEVICE); txd->cmd_status = CMD_OWN | CMD_EOP; // 触发发送 iowrite32(TX_START, priv->regs + TX_REG); priv->tx_idx = (priv->tx_idx + 1) % TX_RING_SIZE; } ``` #### 3. 中断处理(NAPI模式) ```c static irqreturn_t my_interrupt(int irq, void *dev_id) { struct net_device *netdev = dev_id; u32 status = ioread32(priv->regs + INT_STATUS_REG); if (status & RX_INT) { napi_schedule(&priv->napi); // 触发软中断处理接收 } iowrite32(status, priv->regs + INT_STATUS_REG); // 清除中断 return IRQ_HANDLED; } static int my_poll(struct napi_struct *napi, int budget) { int work_done = 0; while (work_done < budget) { struct my_rx_desc *rxd = &priv->rx_ring[priv->rx_idx]; if (!(rxd->status & RXD_OWN)) break; // 构建sk_buff并上传网络栈 struct sk_buff *skb = netdev_alloc_skb(netdev, rxd->size); dma_sync_single_for_cpu(&pdev->dev, rxd->dma_addr, rxd->size, DMA_FROM_DEVICE); skb_copy_to_linear_data(skb, phys_to_virt(rxd->dma_addr), rxd->size); skb_put(skb, rxd->size); netif_receive_skb(skb); work_done++; priv->rx_idx = (priv->rx_idx + 1) % RX_RING_SIZE; } if (work_done < budget) napi_complete_done(napi, work_done); return work_done; } ``` --- ### 三、关键开发要点 1. **DMA环形缓冲区** - 发送/接收环建议大小:`TX_RING_SIZE=256`, `RX_RING_SIZE=512` - 使用`dma_alloc_coherent()`分配对齐内存: ```c priv->tx_ring = dma_alloc_coherent(&pdev->dev, TX_RING_SIZE * sizeof(struct my_tx_desc), &priv->tx_dma, GFP_KERNEL); ``` 2. **PHY配置** - 通过MDIO接口访问PHY寄存器(手册第11章): ```c void mdio_write(struct my_priv *priv, int reg, u16 val) { iowrite32(MDIO_CTRL | (reg << 16) | val, priv->regs + MDIO_REG); while (ioread32(priv->regs + MDIO_REG) & MDIO_BUSY); } ``` 3. **错误处理** - 关键检查点: ```c if (!pci_set_dma_mask(pdev, DMA_BIT_MASK(64))) { pci_set_consistent_dma_mask(pdev, DMA_BIT_MASK(64)); } else if ((err = pci_set_dma_mask(pdev, DMA_BIT_MASK(32))) { dev_err(&pdev->dev, "No suitable DMA available"); goto err_dma; } ``` --- ### 四、调试与测试 1. **日志输出** ```c printk(KERN_DEBUG "TX ring %d: status=%08x\n", idx, ioread32(priv->regs + TX_STATUS_REG)); ``` 2. **用户空间工具** ```bash # 加载驱动 sudo insmod my_82567.ko # 查看内核日志 dmesg | grep my_82567 # 测试网络功能 ethtool -i ethX # 查看驱动信息 ping -c 4 8.8.8.8 ``` 3. **Sysfs接口** 实现`ethtool_ops`结构体支持统计信息查询: ```c static const struct ethtool_ops my_ethtool_ops = { .get_drvinfo = my_get_drvinfo, .get_link = ethtool_op_get_link, }; ``` --- ### 五、参考资源 1. **Linux内核文档** - [Network Device Drivers](https://docs.kernel.org/networking/netdev.html) - [PCI Driver HOWTO](https://docs.kernel.org/PCI/pci.html) 2. **现有驱动参考** - Linux源码`drivers/net/ethernet/intel/e1000e` - 重点文件:`e1000_main.c`, `e1000_hw.h` 3. **调试工具** - `ftrace`跟踪函数调用 - `perf`分析中断频率 - `kgdb`硬件级调试 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值