linux内核中的copy_to_user和copy_from_user(一)

32 篇文章 0 订阅
14 篇文章 0 订阅

linux内核中的copy_to_user和copy_from_user(一)

2017年12月21日 20:07:32 prike 阅读数:4768

linux内核中的copy_to_user和copy_from_user(一)

Kernel version:2.6.14

CPU architecture:ARM920T

Author:ce123(http://blog.csdn.net/ce123)

 

1.copy_from_user

在学习Linux内核驱动的时候,经常会碰到copy_from_user和copy_to_user这两个函数,设备驱动程序中的ioctl函数就经常会用到。这两个函数负责在用户空间和内核空间传递数据。首先看看它们的定义(linux/include/asm-arm/uaccess.h),先看copy_from_user:

 

[plain] view plain copy

  1. static inline unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)  
  2. {  
  3.     if (access_ok(VERIFY_READ, from, n))  
  4.         n = __arch_copy_from_user(to, from, n);  
  5.     else /* security hole - plug it */  
  6.         memzero(to, n);  
  7.     return n;  
  8. }  

先看函数的三个参数:*to是内核空间的指针,*from是用户空间指针,n表示从用户空间想内核空间拷贝数据的字节数。如果成功执行拷贝操作,则返回0,否则返回还没有完成拷贝的字节数。

这个函数从结构上来分析,其实都可以分为两个部分:

  1. 首先检查用户空间的地址指针是否有效;
  2. 调用__arch_copy_from_user函数。

1.1.access_ok

access_ok用来对用户空间的地址指针from作某种有效性检验,这个宏和体系结构相关,在arm平台上为(linux/include/asm-arm/uaccess.h):

 

[plain] view plain copy

  1. #define __range_ok(addr,size) ({ \  
  2.     unsigned long flag, sum; \  
  3.     __chk_user_ptr(addr);   \  
  4.     __asm__("adds %1, %2, %3; sbcccs %1, %1, %0; movcc %0, #0" \  
  5.         : "=&r" (flag), "=&r" (sum) \  
  6.         : "r" (addr), "Ir" (size), "0" (current_thread_info()->addr_limit) \  
  7.         : "cc"); \  
  8.     flag; })  
  9.   
  10. #define access_ok(type,addr,size)   (__range_ok(addr,size) == 0)  

可以看到access_ok中第一个参数type并没有用到,__range_ok的作用在于判断addr+size之后是否还在进程的用户空间范围之内。下面我们具体看一下。这段代码涉及到GCC内联汇编,不懂的朋友可以先看看这篇博客(http://blog.csdn.net/ce123/article/details/8209702)。
(1)unsigned long flag, sum;\\定义两个变量

  • flag:保存结果的变量:非零代表地址无效,零代表地址可以访问。初始存放非零值(current_thread_info()->addr_limit),也就是当前进程的地址上限值。
  • sum:保存要访问的地址范围末端,用于和当前进程地址空间限制数据做比较。

(2)__chk_user_ptr(addr);\\定义是一个空函数
这个函数涉及到__CHECKER__宏的判断,__CHECKER__宏在通过Sparse(Semantic Parser for C)工具对内核代码进行检查时会定义的。在使用make C=1或C=2时便会调用该工具,这个工具可以检查在代码中声明了sparse所能检查到的相关属性的内核函数和变量。

  • 如果定义了__CHECKER__,__chk_user_ptr和__chk_io_ptr在这里只声明函数,没有函数体,目的就是在编译过程中Sparse能够捕捉到编译错误,检查参数的类型。
  • 如果没有定义__CHECKER__,这就是一个空函数。

请看具体的定义(linux/compiler.h):

 

[plain] view plain copy

  1. #ifdef __CHECKER__  
  2. ...  
  3. extern void __chk_user_ptr(void __user *);  
  4. extern void __chk_io_ptr(void __iomem *);  
  5. #else  
  6. ...  
  7. # define __chk_user_ptr(x) (void)0  
  8. # define __chk_io_ptr(x) (void)0  
  9. ...  
  10. #endif  

(3)接下来是汇编:
adds %1, %2, %3
sum = addr + size 这个操作影响状态位(目的是影响是进位标志C),以下的两个指令都带有条件CC,也就是当C=0的时候才执行。

如果上面的加法指令进位了(C=1),则以下的指令都不执行,flag就为初始值current_thread_info()->addr_limit(非0),并返回。
如果没有进位(C=0),就执行下面的指令:
sbcccs %1, %1, %0 
sum = sum - flag - 1,也就是(addr + size) -  (current_thread_info()->addr_limit) - 1,操作影响符号位。
如果(addr + size) >= (current_thread_info()->addr_limit) - 1,则C=1
如果(addr + size) < (current_thread_info()->addr_limit) - 1,则C=0
当C=0的时候执行以下指令,否则跳过(flag非零)。
movcc %0, #0
flag = 0,给flag赋值0。

综上所述:__range_ok宏其实等价于:

  • 如果(addr + size) >= (current_thread_info()->addr_limit) - 1,返回非零值
  • 如果(addr + size) < (current_thread_info()->addr_limit),返回零

而access_ok就是检验将要操作的用户空间的地址范围是否在当前进程的用户地址空间限制中。这个宏的功能很简单,完全可以用C实现,不是必须使用汇编。但于这两个函数使用频繁,就使用汇编来实现部分功能来增加效率。
从这里再次可以认识到,copy_from_user的使用是结合进程上下文的,因为他们要访问“user”的内存空间,这个“user”必须是某个特定的进程。通过上面的源码就知道,其中使用了current_thread_info()来检查空间是否可以访问。如果在驱动中使用这两个函数,必须是在实现系统调用的函数中使用,不可在实现中断处理的函数中使用。如果在中断上下文中使用了,那代码就很可能操作了根本不相关的进程地址空间。其次由于操作的页面可能被换出,这两个函数可能会休眠,所以同样不可在中断上下文中使用。

1.2.__arch_copy_from_user

在深入讲解之前,我们先想一个问题:为什么要使用copy_from_user函数???理论上,内核空间可以直接使用用户空间传过来的指针,即使要做数据拷贝的动作,也可以直接使用memcpy,事实上,在没有MMU的体系架构上,copy_form_user最终的实现就是利用了memcpy。但对于大多数有MMU的平台,情况就有了一些变化:用户空间传过来的指针是在虚拟地址空间上的,它指向的虚拟地址空间很可能还没有真正映射到实际的物理页面上。但这又能怎样呢?缺页导致的异常会透明的被内核予以修复(为缺页的地址空间提交新的物理页面),访问到缺页的指令会继续运行仿佛什么都没有发生一样。但这只是用户空间缺页异常的行为,在内核空间这样却因一场必须被显示的修复,这是由内核提供的缺页异常处理函数的设计模式决定的,其背后的思想后:在内核态中,如果程序试图访问一个尚未提交物理页面的用户空间地址,内核必须对此保持警惕而不能像用户空间那样毫无察觉。

如果内核访问一个尚未被提交物理页面的空间,将产生缺页异常,内核会调用do_page_fault,因为异常发生在内核空间,do_page_fault将调用search_exception_tables在“ __ex_table”中查找异常指令的修复指令,在__arch_copy_from_user函数中经常使用USER宏,这个宏中了定义了“__ex_table”section。

 

linux/include/asm-arm/assembler.h

[plain] view plain copy

  1. #define USER(x...)              \  
  2. 9999:   x;                  \  
  3.     .section __ex_table,"a";        \  
  4.     .align  3;              \  
  5.     .long   9999b,9001f;            \  
  6.     .previous  

该定义中有如下数据;

[plain] view plain copy

  1. .long   9999b,9001f;  

其中9999b对应标号9999处的指令,9001f是9001处的指令,是9999b处指令的修复指令。这样,当标号9999处发生缺页异常时,系统将调用do_page_fault提交物理页面,然后跳到9001继续执行。

如果在驱动程序中不使用copy_from_user而用memcpy来代替,对于上述的情形会产生什么结果呢?当标号9999出发生缺页异常时,系统在“__ex_table”section总将找不到修复地址,因为memcpy没有像copy_from_user那样定义一个“__ex_table”section,此时do_page_fault将通过no_context函数产生Oops。极有可能会看到类似如下信息:

Unable to handle kernel NULL pointer dereference at virtual address 00000fe0

所有为了确保设备驱动程序的安全,应该使用copy_from_user函数而不是memcpy。

下面我们深入分析__arch_copy_from_user函数的实现,该函数是用汇编实现的,定义在linux/arch/arm/lib/uaccess.S文件中。

 

[plain] view plain copy

  1. /* Prototype: unsigned long __arch_copy_from_user(void *to,const void *from,unsigned long n);  
  2.  * Purpose  : copy a block from user memory to kernel memory  
  3.  * Params   : to   - kernel memory  
  4.  *          : from - user memory  
  5.  *          : n    - number of bytes to copy  
  6.  * Returns  : Number of bytes NOT copied.  
  7.  */  
  8. .cfu_dest_not_aligned:  
  9.         rsb ip, ip, #4  
  10.         cmp ip, #2  
  11. USER(       ldrbt   r3, [r1], #1)           @ May fault  
  12.         strb    r3, [r0], #1  
  13. USER(       ldrgebt r3, [r1], #1)           @ May fault  
  14.         strgeb  r3, [r0], #1  
  15. USER(       ldrgtbt r3, [r1], #1)           @ May fault  
  16.         strgtb  r3, [r0], #1  
  17.         sub r2, r2, ip  
  18.         b   .cfu_dest_aligned  
  19.   
  20. ENTRY(__arch_copy_from_user)  
  21.         stmfd   sp!, {r0, r2, r4 - r7, lr}  
  22.         cmp r2, #4  
  23.         blt .cfu_not_enough  
  24.     PLD(    pld [r1, #0]        )  
  25.     PLD(    pld [r0, #0]        )  
  26.         ands    ip, r0, #3  
  27.         bne .cfu_dest_not_aligned  
  28. .cfu_dest_aligned:  
  29.         ands    ip, r1, #3  
  30.         bne .cfu_src_not_aligned  
  31. /*  
  32.  * Seeing as there has to be at least 8 bytes to copy, we can  
  33.  * copy one word, and force a user-mode page fault...  
  34.  */  
  35.   
  36. .cfu_0fupi: subs    r2, r2, #4  
  37.         addmi   ip, r2, #4  
  38.         bmi .cfu_0nowords  
  39. USER(       ldrt    r3, [r1], #4)  
  40.         str r3, [r0], #4  
  41.         mov ip, r1, lsl #32 - PAGE_SHIFT    @ On each page, use a ld/st??t instruction  
  42.         rsb ip, ip, #0  
  43.         movs    ip, ip, lsr #32 - PAGE_SHIFT  
  44.         beq .cfu_0fupi  
  45. /*  
  46.  * ip = max no. of bytes to copy before needing another "strt" insn  
  47.  */  
  48.         cmp r2, ip  
  49.         movlt   ip, r2  
  50.         sub r2, r2, ip  
  51.         subs    ip, ip, #32  
  52.         blt .cfu_0rem8lp  
  53.     PLD(    pld [r1, #28]       )  
  54.     PLD(    pld [r0, #28]       )  
  55.     PLD(    subs    ip, ip, #64         )  
  56.     PLD(    blt .cfu_0cpynopld      )  
  57.     PLD(    pld [r1, #60]       )  
  58.     PLD(    pld [r0, #60]       )  
  59.   
  60. .cfu_0cpy8lp:  
  61.     PLD(    pld [r1, #92]       )  
  62.     PLD(    pld [r0, #92]       )  
  63. .cfu_0cpynopld: ldmia   r1!, {r3 - r6}          @ Shouldnt fault  
  64.         stmia   r0!, {r3 - r6}  
  65.         ldmia   r1!, {r3 - r6}          @ Shouldnt fault  
  66.         subs    ip, ip, #32  
  67.         stmia   r0!, {r3 - r6}  
  68.         bpl .cfu_0cpy8lp  
  69.     PLD(    cmn ip, #64         )  
  70.     PLD(    bge .cfu_0cpynopld      )  
  71.     PLD(    add ip, ip, #64     )  
  72.   
  73. .cfu_0rem8lp:   cmn ip, #16  
  74.         ldmgeia r1!, {r3 - r6}          @ Shouldnt fault  
  75.         stmgeia r0!, {r3 - r6}  
  76.         tst ip, #8  
  77.         ldmneia r1!, {r3 - r4}          @ Shouldnt fault  
  78.         stmneia r0!, {r3 - r4}  
  79.         tst ip, #4  
  80.         ldrnet  r3, [r1], #4            @ Shouldnt fault  
  81.         strne   r3, [r0], #4  
  82.         ands    ip, ip, #3  
  83.         beq .cfu_0fupi  
  84. .cfu_0nowords:  teq ip, #0  
  85.         beq .cfu_finished  
  86. .cfu_nowords:   cmp ip, #2  
  87. USER(       ldrbt   r3, [r1], #1)           @ May fault  
  88.         strb    r3, [r0], #1  
  89. USER(       ldrgebt r3, [r1], #1)           @ May fault  
  90.         strgeb  r3, [r0], #1  
  91. USER(       ldrgtbt r3, [r1], #1)           @ May fault  
  92.         strgtb  r3, [r0], #1  
  93.         b   .cfu_finished  
  94.   
  95. .cfu_not_enough:  
  96.         movs    ip, r2  
  97.         bne .cfu_nowords  
  98. .cfu_finished:  mov r0, #0  
  99.         add sp, sp, #8  
  100.         LOADREGS(fd,sp!,{r4 - r7, pc})  
  101.   
  102. .cfu_src_not_aligned:  
  103.         bic r1, r1, #3  
  104. USER(       ldrt    r7, [r1], #4)           @ May fault  
  105.         cmp ip, #2  
  106.         bgt .cfu_3fupi  
  107.         beq .cfu_2fupi  
  108. .cfu_1fupi: subs    r2, r2, #4  
  109.         addmi   ip, r2, #4  
  110.         bmi .cfu_1nowords  
  111.         mov r3, r7, pull #8  
  112. USER(       ldrt    r7, [r1], #4)           @ May fault  
  113.         orr r3, r3, r7, push #24  
  114.         str r3, [r0], #4  
  115.         mov ip, r1, lsl #32 - PAGE_SHIFT  
  116.         rsb ip, ip, #0  
  117.         movs    ip, ip, lsr #32 - PAGE_SHIFT  
  118.         beq .cfu_1fupi  
  119.         cmp r2, ip  
  120.         movlt   ip, r2  
  121.         sub r2, r2, ip  
  122.         subs    ip, ip, #16  
  123.         blt .cfu_1rem8lp  
  124.     PLD(    pld [r1, #12]       )  
  125.     PLD(    pld [r0, #12]       )  
  126.     PLD(    subs    ip, ip, #32     )  
  127.     PLD(    blt .cfu_1cpynopld      )  
  128.     PLD(    pld [r1, #28]       )  
  129.     PLD(    pld [r0, #28]       )  
  130.   
  131. .cfu_1cpy8lp:  
  132.     PLD(    pld [r1, #44]       )  
  133.     PLD(    pld [r0, #44]       )  
  134. .cfu_1cpynopld: mov r3, r7, pull #8  
  135.         ldmia   r1!, {r4 - r7}          @ Shouldnt fault  
  136.         subs    ip, ip, #16  
  137.         orr r3, r3, r4, push #24  
  138.         mov r4, r4, pull #8  
  139.         orr r4, r4, r5, push #24  
  140.         mov r5, r5, pull #8  
  141.         orr r5, r5, r6, push #24  
  142.         mov r6, r6, pull #8  
  143.         orr r6, r6, r7, push #24  
  144.         stmia   r0!, {r3 - r6}  
  145.         bpl .cfu_1cpy8lp  
  146.     PLD(    cmn ip, #32         )  
  147.     PLD(    bge .cfu_1cpynopld      )  
  148.     PLD(    add ip, ip, #32     )  
  149.   
  150. .cfu_1rem8lp:   tst ip, #8  
  151.         movne   r3, r7, pull #8  
  152.         ldmneia r1!, {r4, r7}           @ Shouldnt fault  
  153.         orrne   r3, r3, r4, push #24  
  154.         movne   r4, r4, pull #8  
  155.         orrne   r4, r4, r7, push #24  
  156.         stmneia r0!, {r3 - r4}  
  157.         tst ip, #4  
  158.         movne   r3, r7, pull #8  
  159. USER(       ldrnet  r7, [r1], #4)           @ May fault  
  160.         orrne   r3, r3, r7, push #24  
  161.         strne   r3, [r0], #4  
  162.         ands    ip, ip, #3  
  163.         beq .cfu_1fupi  
  164. .cfu_1nowords:  mov r3, r7, get_byte_1  
  165.         teq ip, #0  
  166.         beq .cfu_finished  
  167.         cmp ip, #2  
  168.         strb    r3, [r0], #1  
  169.         movge   r3, r7, get_byte_2  
  170.         strgeb  r3, [r0], #1  
  171.         movgt   r3, r7, get_byte_3  
  172.         strgtb  r3, [r0], #1  
  173.         b   .cfu_finished  
  174.   
  175. .cfu_2fupi: subs    r2, r2, #4  
  176.         addmi   ip, r2, #4  
  177.         bmi .cfu_2nowords  
  178.         mov r3, r7, pull #16  
  179. USER(       ldrt    r7, [r1], #4)           @ May fault  
  180.         orr r3, r3, r7, push #16  
  181.         str r3, [r0], #4  
  182.         mov ip, r1, lsl #32 - PAGE_SHIFT  
  183.         rsb ip, ip, #0  
  184.         movs    ip, ip, lsr #32 - PAGE_SHIFT  
  185.         beq .cfu_2fupi  
  186.         cmp r2, ip  
  187.         movlt   ip, r2  
  188.         sub r2, r2, ip  
  189.         subs    ip, ip, #16  
  190.         blt .cfu_2rem8lp  
  191.     PLD(    pld [r1, #12]       )  
  192.     PLD(    pld [r0, #12]       )  
  193.     PLD(    subs    ip, ip, #32     )  
  194.     PLD(    blt .cfu_2cpynopld      )  
  195.     PLD(    pld [r1, #28]       )  
  196.     PLD(    pld [r0, #28]       )  
  197.   
  198. .cfu_2cpy8lp:  
  199.     PLD(    pld [r1, #44]       )  
  200.     PLD(    pld [r0, #44]       )  
  201. .cfu_2cpynopld: mov r3, r7, pull #16  
  202.         ldmia   r1!, {r4 - r7}          @ Shouldnt fault  
  203.         subs    ip, ip, #16  
  204.         orr r3, r3, r4, push #16  
  205.         mov r4, r4, pull #16  
  206.         orr r4, r4, r5, push #16  
  207.         mov r5, r5, pull #16  
  208.         orr r5, r5, r6, push #16  
  209.         mov r6, r6, pull #16  
  210.         orr r6, r6, r7, push #16  
  211.         stmia   r0!, {r3 - r6}  
  212.         bpl .cfu_2cpy8lp  
  213.     PLD(    cmn ip, #32         )  
  214.     PLD(    bge .cfu_2cpynopld      )  
  215.     PLD(    add ip, ip, #32     )  
  216.   
  217. .cfu_2rem8lp:   tst ip, #8  
  218.         movne   r3, r7, pull #16  
  219.         ldmneia r1!, {r4, r7}           @ Shouldnt fault  
  220.         orrne   r3, r3, r4, push #16  
  221.         movne   r4, r4, pull #16  
  222.         orrne   r4, r4, r7, push #16  
  223.         stmneia r0!, {r3 - r4}  
  224.         tst ip, #4  
  225.         movne   r3, r7, pull #16  
  226. USER(       ldrnet  r7, [r1], #4)           @ May fault  
  227.         orrne   r3, r3, r7, push #16  
  228.         strne   r3, [r0], #4  
  229.         ands    ip, ip, #3  
  230.         beq .cfu_2fupi  
  231. .cfu_2nowords:  mov r3, r7, get_byte_2  
  232.         teq ip, #0  
  233.         beq .cfu_finished  
  234.         cmp ip, #2  
  235.         strb    r3, [r0], #1  
  236.         movge   r3, r7, get_byte_3  
  237.         strgeb  r3, [r0], #1  
  238. USER(       ldrgtbt r3, [r1], #0)           @ May fault  
  239.         strgtb  r3, [r0], #1  
  240.         b   .cfu_finished  
  241.   
  242. .cfu_3fupi: subs    r2, r2, #4  
  243.         addmi   ip, r2, #4  
  244.         bmi .cfu_3nowords  
  245.         mov r3, r7, pull #24  
  246. USER(       ldrt    r7, [r1], #4)           @ May fault  
  247.         orr r3, r3, r7, push #8  
  248.         str r3, [r0], #4  
  249.         mov ip, r1, lsl #32 - PAGE_SHIFT  
  250.         rsb ip, ip, #0  
  251.         movs    ip, ip, lsr #32 - PAGE_SHIFT  
  252.         beq .cfu_3fupi  
  253.         cmp r2, ip  
  254.         movlt   ip, r2  
  255.         sub r2, r2, ip  
  256.         subs    ip, ip, #16  
  257.         blt .cfu_3rem8lp  
  258.     PLD(    pld [r1, #12]       )  
  259.     PLD(    pld [r0, #12]       )  
  260.     PLD(    subs    ip, ip, #32     )  
  261.     PLD(    blt .cfu_3cpynopld      )  
  262.     PLD(    pld [r1, #28]       )  
  263.     PLD(    pld [r0, #28]       )  
  264.   
  265. .cfu_3cpy8lp:  
  266.     PLD(    pld [r1, #44]       )  
  267.     PLD(    pld [r0, #44]       )  
  268. .cfu_3cpynopld: mov r3, r7, pull #24  
  269.         ldmia   r1!, {r4 - r7}          @ Shouldnt fault  
  270.         orr r3, r3, r4, push #8  
  271.         mov r4, r4, pull #24  
  272.         orr r4, r4, r5, push #8  
  273.         mov r5, r5, pull #24  
  274.         orr r5, r5, r6, push #8  
  275.         mov r6, r6, pull #24  
  276.         orr r6, r6, r7, push #8  
  277.         stmia   r0!, {r3 - r6}  
  278.         subs    ip, ip, #16  
  279.         bpl .cfu_3cpy8lp  
  280.     PLD(    cmn ip, #32         )  
  281.     PLD(    bge .cfu_3cpynopld      )  
  282.     PLD(    add ip, ip, #32     )  
  283.   
  284. .cfu_3rem8lp:   tst ip, #8  
  285.         movne   r3, r7, pull #24  
  286.         ldmneia r1!, {r4, r7}           @ Shouldnt fault  
  287.         orrne   r3, r3, r4, push #8  
  288.         movne   r4, r4, pull #24  
  289.         orrne   r4, r4, r7, push #8  
  290.         stmneia r0!, {r3 - r4}  
  291.         tst ip, #4  
  292.         movne   r3, r7, pull #24  
  293. USER(       ldrnet  r7, [r1], #4)           @ May fault  
  294.         orrne   r3, r3, r7, push #8  
  295.         strne   r3, [r0], #4  
  296.         ands    ip, ip, #3  
  297.         beq .cfu_3fupi  
  298. .cfu_3nowords:  mov r3, r7, get_byte_3  
  299.         teq ip, #0  
  300.         beq .cfu_finished  
  301.         cmp ip, #2  
  302.         strb    r3, [r0], #1  
  303. USER(       ldrgebt r3, [r1], #1)           @ May fault  
  304.         strgeb  r3, [r0], #1  
  305. USER(       ldrgtbt r3, [r1], #1)           @ May fault  
  306.         strgtb  r3, [r0], #1  
  307.         b   .cfu_finished  
  308.   
  309.         .section .fixup,"ax"  
  310.         .align  0  
  311.         /*  
  312.          * We took an exception.  r0 contains a pointer to  
  313.          * the byte not copied.  
  314.          */  
  315. 9001:       ldr r2, [sp], #4            @ void *to  
  316.         sub r2, r0, r2          @ bytes copied  
  317.         ldr r1, [sp], #4            @ unsigned long count  
  318.         subs    r4, r1, r2          @ bytes left to copy  
  319.         movne   r1, r4  
  320.         blne    __memzero  
  321.         mov r0, r4  
  322.         LOADREGS(fd,sp!, {r4 - r7, pc})  
  323.         .previous  

我们将在另一篇博文中详细分析该函数。

forward from:

https://blog.csdn.net/prike/article/details/78867270

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值