描述
char *和char数组真的相同吗?我们以实例为证:
typedef struct
{
char * s1;
char * s2;
}PARAM,*PPARAM;
int main(int argc, char *argv[])
{
PARAM pa1,pb1;
pa1.s1 = "abcd";
pa1.s2 = "ABCD";
memcpy(&pb1,&pa1,sizeof(PARAMA));
printf("%s\n",pb1.s1);
printf("%s\n",pb1.s2);
}
打印出的结果为abcd和ABCD
typedef struct
{
char s1[15];
char s2[15];
}PARAM,*PPARAM;
int main(int argc, char *argv[])
{
PARAM pa2,pb2;
strcpy(pa2.s1,"abcd");
strcpy(pa2.s2,"ABCD");
memcpy(&pb2,&pa2,sizeof(PARAMA));
printf("%s\n",pb2.s1);
printf("%s\n",pb2.s2);
}
结 果同样。那么我们是否真的将二者视为相同呢?如果我们不是单纯的memcpy,而是建立一个socket,将PARAM类型的参数pa1作为send的参 数传入,然后在另一端recv,哈哈,发现根本没有得到abcd和ABCD,但数将pa2数组形式的传入send,另一个进程recv得到的就是abcd 和ABCD了,这是为什么?实际上,char *只是一个指,仅仅是一个unsigned long,那么我们看看pa1,内存中实际就8个字节(32位机器),两个指针,一个4个字节,我们传入send的也就是两个指针了,而对于char数组 pa2,它的内存表示就是s1的15个字节而s2的15个字节连续排放,整个结构就是实实在在的数据,我们传入send就将s1和s2的内容一块传送出去 了,而不仅仅只是传送的指针,那么对于上面的两个带有main的例子为何结果一样呢?想想它们可是在同一地址空间,对于char *表示的结构,s1代表一个指针,指向一个该进程地址空间的内存地址,当我们把这个指针复制给另一个结构时,该指针并没有变,因为处于同一个地址空间,所 以该指针指向的地址的数据还是原来的数据,因此会出现一样的结果,而对于socket网络传输,有send和recv两个进程,地址空间不同,你只把地址 传过去,该地址在recv进程指向的就不是abcd或者ABCD了,地址空间变了,因此要想得到正确的结果,必须注意保证穿过去的是实实在在的数据而不是 一个地址,这个进程地址空间的地址在别的进程的地址空间没有任何意义。
简单说说copy_from_user,虽然并没有垮地址空间,但是毕竟从用户空间进入了内核空间,只要保证copy_from_user是在调用进程的 上下文复制本身就不会出错,但是考虑到复制过去的数据让谁用,比如是驱动程序用,而该驱动可能要在中断中使用该数据,进一步中断的执行是在任意进程的上下 文,如果你在copy_from_user的时候只传入一个该进程地址空间的一个指针,那么发生中断的时候调用copy_from_user的进程正好不 是当前进程,当前进程是另一个进程P,这时中断处理程序要用用户传过来的数据了,该数据是个指针,那么中断就会从当前进程的地址空间取那个指针指向的数 据,这当然要出错了,此地址空间不是彼地址空间。copy_from_user本身是没有问题的,就算copy_from_user被抢占,由于页目录被 切换了,地址空间也随着改变。内核空间访问用户空间也是没有任何问题的,书上或者文档上说内核空间不要访问用户空间只是为了安全,实际上是可以随意访问 的,不信的话尝试一下下面的:
#include #include static __init int test_init(void) { char * s = (char *)0x8048264; int i = (int)*s; printk("%02X",i); return 0; } static __exit void test_exit(void) { return ; } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("Dual BSD/GPL"); MODULE_AUTHOR("Zhaoya"); MODULE_DESCRIPTION("kill init"); MODULE_VERSION("Ver 0.1");那个
0x8048264 就是我用objdump查找到的/sbin/insmod的一个地址,objdump中显示的是12,那么加载模块后printk出来的也是12,为何查 找/sbin/insmod呢?因为加载模块以及模块的初始化函数是运行在insmod的进程上下文的,因此可以认为test_init的当前进程就是 insmod,这样可以证明内核完全可以随意访问用户空间,如果想玩更猛的,你可以不光读,再往那个地址写点东西,看看有何效果,内核线程虽然一般都在开 始时放弃了继承的地址空间mm_struct,但是它是可以访问的,它访问的地址空间就是创建该内核进程那个用户进程的空间mm_struct或者是刚刚 切出去的用户进程的mm_struct,如果内核线程随意写那个空间,用户进程一点脾气也没有,谁让它倒霉,即使内核线程release了继承的mm,那 只是还给了slab,内核线程想访问哪里,仅仅依赖有没有页表项以及页面是否在内存,所谓刑不上大夫,不过一般不会出现这么恨毒的内核线程,除非是病毒, 另外smp下内核线程的pgd一般都是swapdir,那是没有任何用户空间映射的。上面的内核线程访问用户空间的前提是它访问的空间恰好在页表项中有记 录,若没有,即使内核线程也甭想了,看看do_page_fault的处理:
if (in_atomic() || !mm)//没有mm还想访问用户空间就完蛋
goto bad_area_nosemaphore;
我们再看一下切换的细节:
static inline task_t * context_switch(runqueue_t *rq, task_t *prev, task_t *next) { struct mm_struct *mm = next->mm; struct mm_struct *oldmm = prev->active_mm; if (unlikely(!mm)) {//内核线程的情形 next->active_mm = oldmm; atomic_inc(&oldmm->mm_count); enter_lazy_tlb(oldmm, next);//单cpu实际什么也没有做 } else switch_mm(oldmm, mm, next);//这个才切换页目录,由于所有进程的内核部分的地址空间相同,故没有必要切换了,那么内核线程实际还是用的切出去的用户进程的空间,因为pgd没有变,内核线程想蹂躏该用户进程,easy! if (unlikely(!prev->mm)) { prev->active_mm = NULL; WARN_ON(rq->prev_mm); rq->prev_mm = oldmm; } switch_to(prev, next, prev); return prev; }
因此,不要过分的死记硬背教条,什么char*和char数组一样啦,内核不能访问用户空间是因为它没有用户地址空间啦,关键是要理解为何这么做,其实想要完全的保证什么谁也做不到,只能彼此有个约定,大家都遵守。
打开APP阅读更多精彩内容