copy_from_user探讨

 

#include <linux.h>

1 用户一般的数据操作只能在用户态进行;要操作内核态数据,必须利用标准的接口实现。
   有很多方法可以读取或者修改内核态的数据。
   1.1 可以利用动态模块,向用户态一样操作核心态数据结构
   1.2 可以利用标准的系统调用、添加的系统调用来操作核心态数据
   1.3 利用procfs读取、写入核心态数据 [sysctl系统调用]
   1.4 利用sysfs读取、写入核心态数据
   1.5 利用seq_file接口操作核心态数据 [自己添加内核操作方法]
   1.6 利用kprobe、jprobe、jretprobe调试技术操作核心态数据
   1.7 利用netlink钩子函数操作核心态数据结构
   1.8 利用访问设备文件的方式操作核心态数据结构
   .....
2  内核态和用户态区别
   内核态的出现源于保护模式,从本质上讲linux系统不相信用户、也不认为用户有能力能很好的利用os所提供 
   的强大的资源管理能力。为此OS运行在与用户隔离的空间中,运行级别为0,所有进程的内核态数据空间都是一样
   的,因为那里跑的是操作系统的代码,执行基本的资源管理任务,线性地址空间位于oxc000 0000以上,
   内存映射方式为:实际内存=线性内存-3G;用户态属于每个进程的私有空间[这也是进程间会有差别的原因],我
   们一 般打交道使用的空间都是用户空间,用户空间的管理依靠于task_strut的mm内存管理单元,其将内存划分
   为若干内存区域(vm_area_struct),然后依靠页表来管理这些上述的内存区域,用户空间是可以被内存交换换进换
   出的,而内核空间显然不能被换出...
3 内核态用户态交互的接口
   1 中描述了大量的内核态交互方法,这好比linux系统的底层不同文件系统的访问[其在vfs层总是归结于同一系统
   调用read和write操作]。内核数据交互也是类似,不过与fs访问正好相反,上面大量的交互方式都归结于下面两个
   最基本的函数[copy_from_user和copy_to_user]。所以摁其咽喉,方能掌握本质,分析分析copy_from_user
   函数
4 copy_from_user()详解    2.6.35.3内核版本
4-1  arch/x86/include/asm/Uaccess_32.h
  static inline unsigned long __must_check copy_from_user(void *to,
       const void __user *from,
       unsigned long n)
{
 int sz = __compiletime_object_size(to);   //宏定义

 if (likely(sz == -1 || sz >= n))
  n = _copy_from_user(to, from, n);
 else
  copy_from_user_overflow();

 return n;
}
解释:先判断内核空间to的空间是否满足拷贝数据n大小,不满足发出WARN(1, "Buffer overflow detected!\n");
4-2 _copy_from_user(to,from,n)
/**
 * copy_from_user: - Copy a block of data from user space.
 * @to:   Destination address, in kernel space.
 * @from: Source address, in user space.
 * @n:    Number of bytes to copy.
 *
 * Context: User context only.  This function may sleep.
 *
 * Copy data from user space to kernel space.
 *
 * Returns number of bytes that could not be copied.
 * On success, this will be zero.
 *
 * If some data could not be copied, this function will pad the copied
 * data to the requested size using zero bytes.
 */
unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
 if (access_ok(VERIFY_READ, from, n))
  n = __copy_from_user(to, from, n);
 else
  memset(to, 0, n);
 return n;
}
解释:注释非常清楚,access_ok()检查用户空间合理性[不超过0xc000 0000],userspace映射问题后面在看
4-3 __copy_from_user(to,from,n)   arch/x86/include/asm/Uaccess_32.h
/*
 * An alternate version - __copy_from_user_inatomic() - may be called from
 * atomic context and will fail rather than sleep.  In this case the
 * uncopied bytes will *NOT* be padded with zeros.  See fs/filemap.h
 * for explanation of why this is needed.
 */
static __always_inline unsigned long
__copy_from_user(void *to, const void __user *from, unsigned long n)
{
 might_fault();
 if (__builtin_constant_p(n)) {
  unsigned long ret;

  switch (n) {
  case 1:
   __get_user_size(*(u8 *)to, from, 1, ret, 1);
   return ret;
  case 2:
   __get_user_size(*(u16 *)to, from, 2, ret, 2);
   return ret;
  case 4:
   __get_user_size(*(u32 *)to, from, 4, ret, 4);
   return ret;
  }
 }
 return __copy_from_user_ll(to, from, n);
}
解释:首先查看n是否为固定值1,2,4字节,如果是固定大小,则操作简单;否则调用通用拷贝函数,适合于大块传输
4-4  __copy_from_user_ll(to, from, n)  /arch/x86/lib
unsigned long __copy_from_user_ll(void *to, const void __user *from,
     unsigned long n)
{
 if (movsl_is_ok(to, from, n))
  __copy_user_zeroing(to, from, n);
 else
  n = __copy_user_zeroing_intel(to, from, n);
 return n;
}
解释:首先判断是否需要大规模数据拷贝,一般返回1
注:linux采用AT&T编码方式,左边值为原操作数,右边值为目的操作数,与intel编码方式不同
4-5 __copy_user_zeroing(to, from, size)      /arch/x86/lib  进入copy的关键
#define  __copy_user_zeroing(to, from, size)    \
do {         \
 int __d0, __d1, __d2;      \
 __asm__ __volatile__(      \                    #注:以4字节做为成串传送的基本单位
  " cmp  $7,%0\n"     \                            #比较size是否大于7,即判断是否需要成串传送,cmp中右为原操作数
  " jbe  1f\n"     \                                    #如果小于7,跳到1处,以单字节作为传送单位
  " movl %1,%0\n"     \                 #ecx=to地址,此时ecx大于7字节,需要把余8单字节传,其余串传ecx=n
  " negl %0\n"     \                                  #ecx取补码,   正数补码为自身
  " andl $7,%0\n"     \                             #ecx=ecx%8,显然操作前ecx=n,且andl中右为原操作数
  " subl %0,%3\n"     \                            #寄存器X-=ecx,ecx为模8取余,显然此时寄存器X=n - n%8
  "4: rep; movsb\n"     \                          #余8剩余字节按照字节拷贝,显然此时ecx为n模8取余的值
  " movl %3,%0\n"     \                           #ecx=该寄存器X值,X值应为n-n%8,movl中左为原操作数
  " shrl $2,%0\n"     \                              #然后ecx=ecx/4,此时ecx为movsl的次数,shrl中右为原操作数
  " andl $3,%3\n"     \                             #该寄存器X=X%4,andl中右为原操作数

  " .align 2,0x90\n"    \                            #
  "0: rep; movsl\n"     \                           #按照四字节倍数拷贝,每次拷贝4个字节,   此时ecx=n/4
  " movl %3,%0\n"     \                           #ecx值设为n%4,   此时该寄存器X=n%4
  "1: rep; movsb\n"     \                          #以单字节拷贝
  "2:\n"       \              
  ".section .fixup,\"ax\"\n"    \
  "5: addl %3,%0\n"  \   #寄存器X值[n-n%8] + ecx值[n%8-已拷贝字节] -->  ecx,addl,subl中右为原操作数
  " jmp 6f\n"     \                                     #跳转到后边标号6处,此时ecx显然为剩余拷贝的字节数
  "3: lea 0(%3,%0,4),%0\n"    \                #ecx=ecx*4+n%4,ecx=在出错时剩余拷贝的字节数
  "6: pushl %0\n"     \                              #ecx压入栈,待后面出错返回时使用
  " pushl %%eax\n"     \                           #eax寄存器压栈,用0填充内核剩余空间时用eax值填
  " xorl %%eax,%%eax\n"    \                  #eax清0
  " rep; stosb\n"     \                                #此时将内核剩余复制空间清0
  " popl %%eax\n"     \                             #恢复eax值
  " popl %0\n"     \                                   #ecx=剩余未拷贝的字节数
  " jmp 2b\n"     \                                     #跳到前面2标号处,即退出内存拷贝
  ".previous\n"      \    
  ".section __ex_table,\"a\"\n"    \              #专用的异常地址表,用于拷贝过程中的异常恢复
  " .align 4\n"     \                                     #4字节为单位对其
  " .long 4b,5b\n"     \                               #标号5 为标号4的异常处理程序地址
  " .long 0b,3b\n"     \                               #标号3 为标号0的异常处理程序地址
  " .long 1b,6b\n"     \                               #标号6 为标号1的异常处理程序地址
  ".previous"      \
  : "=&c"(size), "=&D" (__d0), "=&S" (__d1), "=r"(__d2) \
  : "3"(size), "0"(size), "1"(to), "2"(from)  \
  : "memory");      \
} while (0)
解释:
1 变量  绑定寄存器  初始值 输出值 
   0      ecx           size     size
   1      edi            to指针  __do变量
   2      esi            from    __d1变量
   3      寄存器        size     __d2变量
2 分析
2-1
  gnu的gcc和ld支持四个段:text段、data段、fixup段、__ex_table段。
  fixup段用于异常发生后的恢复操作,和text段没有太大差别
  __ex_tabel段用于异常地址表
2-2
在cpu进行访址的时候,内核空间和用户空间使用的都是线性地址,cpu在访址的过程中会自动完成从线性地址到物理地址的转换[用户态、内核态都得依靠进程页表完成转换],而合理的线性地址意味着:该线性地址位于该进程task_struct->mm虚存空间的某一段vm_struct_mm中,而且建立线性地址到物理地址的映射,即线性地址对应内容在物理内存中。如果访存失败,有两种可能:该线性地址存在在进程虚存区间中,但是并未建立于物理内存的映射,有可能是交换出去,也有可能是刚申请到线性区间[内核是很会偷懒的],要依靠缺页异常去建立申请物理空间并建立映射;第2种可能是线性地址空间根本没有在进程虚存区间中,这样就会出现常见的坏指针,就会引发常见的段错误[也有可能由于访问了无权访问的空间造成保护异常]。如果坏指针问题发生在用户态,最严重的就是杀死进程[最常见的就是在打dota时候出现的大红X,然后dota程序结束],如果发生在内核态,整个系统可能崩溃[xp的蓝屏很可能就是这种原因形成的]。所以linux当然不会任由这种情况的发生,其措施如下:
    linux内核对于可能发生问题的指令都会准备"修复地址",比如前面的fixup部分,而且遵循谁使用这些指令,谁负责修复工作的原则。比如前面的代码中,标号5即为标号4的修复指令,3为0,6为1的修复指令。在编译过程中,编译器会将5,4等的地址对应的存入struct exception_table_entry{unsigned long insn,fixup;}中。insn即可能为4的地址,而fixup可能为5的地址,如果4为坏地址[即该地址并未在虚存区间中],则在页面异常处理过程中,会转入bad_area处,如果发生在用户态直接杀死进程即可。如果发生在内核态,首先通过search_exception_table查找异常处理表exception_table。即找到某一个exception_table_entry,假设其insn=标号4地址,fixup=标号5地址.内核将发生:
regs->ip=fixup,即通过修改当前的内核地址,从而将内核从死亡的边缘拉回来,通过标号5地址处的修复工作从而全身而退。

   欢迎探讨、交流!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值