首先我们来看一段代码
//创建进程
int sys_process_create( const char* cmdline, const char** var, void* cinfo, uint* retp )
{
process_t* p;
int len;
p = kmalloc( sizeof(process_t) );
len = strlen( cmdline );
memcpy( p->cmdline, cmdline, len+1 );
...
}
创建进程的时候,需要拷贝字符串cmdline。那么要拷贝这个串,首先要得到长度。一般来说,通过库函数strlen可以最快得到字符串的长度。
但是如果cmdline指向一个不可用的地址,例如0xF0000000,那么就会触发异常。内核态下发生异常是不可能由用户态捕捉处理的,也只能结束线程。但是别忘了,上面代码中的p指向的内核内存还没有释放,这就导致了内存泄漏。
但是我们不会写一个safe_strlen来不断检查内存是否是用户内存吧。这样严重影响效率。
那么Linux是如何解决这个问题的呢?
Linux内核下复制字符串采用copy_strings来完成,我们先看一下代码:
int copy_strings(int argc,char ** argv, struct linux_binprm *bprm)
{
struct page *kmapped_page = NULL;
char *kaddr = NULL;
int ret;
while (argc-- > 0) {
char *str;
int len;
unsigned long pos;
if (get_user(str, argv+argc) ||
!(len = strnlen_user(str, bprm->p))) {
ret = -EFAULT;
goto out;
}
可见,linux是使用一个名为strnlen_user的函数。
strnlen_user是用汇编写的,其中代码如下
long direct_strnlen_user(const char __user *s, long n)
{
unsigned long mask = -__addr_ok(s);
unsigned long res, tmp;
might_sleep();
__asm__ __volatile__(
" testl %0, %0\n"
" jz 3f\n"
" andl %0,%%ecx\n"
"0: repne; scasb\n"
" setne %%al\n"
" subl %%ecx,%0\n"
" addl %0,%%eax\n"
"1:\n"
".section .fixup,\"ax\"\n"
"2: xorl %%eax,%%eax\n"
" jmp 1b\n"
"3: movb $1,%%al\n"
" jmp 1b\n"
".previous\n"
".section __ex_table,\"a\"\n"
" .align 4\n"
" .long 0b,2b\n"
".previous"
:"=r" (n), "=D" (s), "=a" (res), "=c" (tmp)
:"0" (n), "1" (s), "2" (0), "3" (mask)
:"cc");
return res & mask;
}
这里设置了一个异常处理用的table,即一旦发生异常的时候,strncpy就返回0值。这样是很好的,但拷贝函数不只是有这个,还有memcpy,strcpy等。而且有时候需要对非法访问内存进行一定的处理。因此如果在内核下可以使用_try和_catch处理发生的异常,能够容易把握代码结构和灵活响应。使用异常处理机制比读写前检查效率更高。因为一般来说,100多次系统调用,可能有一次或者最多一次的参数是不正确的,那么对于这99次读写检查,cpu就做了很多无用功了。
当然,_try和_catch要做得处理速度相当好,例如进入_try时,只需要把异常处理句柄放进异常处理链表,然后马上继续执行系统调用的代码即可。
目前没有研究过Windows如何处理这个问题。但我发觉不少朋友自己写的OS不是不作检查,就是每次都对读写范围进行有效性检查,前者安全性不好,后者效率低下,代码繁琐。
没有评论:
发表评论