char *和char数组的区别(深拷贝和浅拷贝的观点)以及内核访问用户空间

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数组一样啦,内核不能访问用户空间是因为它没有用户地址空间啦,关键是要理解为何这么做,其实想要完全的保证什么谁也做不到,只能彼此有个约定,大家都遵守。

最后要说明的是,linux中内核要想访问用户空间必须首先获得用户空间的地址空间引用,也就是mm_struct,作为一个例子,请看2.6内核中的异 步io中,执行异步io操作的是工作队列,而工作队列在内核线程上下文运行,另外目前异步io的条件是必须是O_DIRECT形式的io,而O_DIRECT形式的io并不用内核io缓存,这样的话io期间必须直接将用户空间缓存直接写入磁盘或者磁盘数据直接读入用户空间缓存,工作队列执行异步io的时候,作为一种O_DIRECT形式的io必须访问用户的缓存,而用户缓存是在用户进程的地址空间中的,这样的话,在工作队列执行异步io的时候,必须先将地址空间切换到需要异步io的用户进程的地址空间,也即切换mm_struct,请看:

static void aio_kick_handler(void *data)

{

         struct kioctx *ctx = data;

         mm_segment_t oldfs = get_fs();

         int requeue;

         set_fs(USER_DS);

         use_mm(ctx->mm);  //切换地址空间

         spin_lock_irq(&ctx->ctx_lock);

         requeue =__aio_run_iocbs(ctx);

         unuse_mm(ctx->mm);

         spin_unlock_irq(&ctx->ctx_lock);

         set_fs(oldfs);

...

}

static void use_mm(struct mm_struct *mm)

{

         struct mm_struct *active_mm;

         struct task_struct *tsk = current;

         task_lock(tsk);

         tsk->flags |= PF_BORROWED_MM;

         active_mm = tsk->active_mm;

         atomic_inc(&mm->mm_count);

         tsk->mm = mm;

         tsk->active_mm = mm;

         activate_mm(active_mm, mm);  //具体切换,在x86上也就是切换cr3寄存器

         task_unlock(tsk);

         mmdrop(active_mm);  //恢复mm的引用计数

}

const和volatile--蹂躏编译器

这两个关键字是c语言中比较重要的,但是开发者往往过分关注了它们以至于迷失于其中。其实它们并没有代表多少复杂的逻辑,而仅仅是具有编译而非运行意义的一些东西,它们更像是一种数据类型。const意义在于它所定义的数据不能通过内存操作修改,但是却可以通过非内存操作修改,而volatile仅仅是禁 止了编译优化,我们来看一下const,试一下以下的代码:

const int value = 3;

int main( void )

{

    int *p = (int *)&value;

    *p = 6;  //事实上修改了const变量value

    printf("a-addr:%d ---value:%d/n", &value, value );

    printf("p-addr:%d ---value:%d/n",p, *p );

}

编 译没有问题,但是运行时会报错,在*p=6的地方。我们并没有显式修改const变量value但是为何会在*p=6的地方报错呢?这说明const对变 量的约束比我们想象的更加严格。*p=6显然是一次访问内存的操作,如果访问内存出错,我们首先要考虑的就是:1.该地址未被映射进进程的地址空间;2. 写了只读内存。根据我们的代码可以看出1是不可能的,因此我们考虑原因2,为了确定真的就是原因2引起的错误,我们将代码更改如下:

const int value = 3;

int main( void )

{

    int *p = (int *)&value;

    MEMORY_BASIC_INFORMATION mi={0};

    size_t n = VirtualQuery(p,&mi,sizeof(mi));

    *p = 6;

}

*p = 6的地方下断点,我们看mi的内容,mi是一个MEMORY_BASIC_INFORMATION结构体,其中Protect字段表示这段内存的保护属性(关于VirtualQuery和MEMORY_BASIC_INFORMATION结构,且查MSDN),我调试的结果mi的Protect字段为 0x02,即PAGE_READONLY,由此可见,const定义的变量直接设置了内存的保护属性,而应用程序所谓的对变量的修改实际上就是修改内存, 因为变量总是被放置到内存中的。设置了const变量的内存只读属性最终被反映到了页表项上,从而在写只读页的时候会导致通用保护异常。这就是const 的意义,你无论如何不能“传统”的修改const变量,但是这个变量真的就不能修改吗?const变量所限制的仅仅是应用程序,因为它仅仅设置了应用程序的用户地址空间的虚拟内存的对应页表项为只读,但是如果我们找到该const变量所在的页面,然后再映射到不同的虚拟地址或者直接映射到内核空间,那么就完全可以更改const变量了,实际上这不是一中标准的做法,也不是推荐的做法,如果仅仅想钻牛角尖的话倒是可以一试,这里想说的是,const提供的只 是对该进程用户空间对应的虚拟内存的写保护,一个变量是const的唯一意义就是告诉世界应用程序不能更改此变量,但是别的实体却可以更改,比如一个寄存器,在某些可以将io映射到虚拟的机器上,我就可以将这个寄存器赋给一个变量,这个变量是const代表应用程序不能更改寄存器的值,但是内核却可以,设 备可能也可以。至于怎么寻找const变量所在的页面这里不讨论,可以参考linux的物理内存和虚拟内存的线性映射,如果这个const变量所在的页面属于物理内存的前896M比如是a,那么虚拟地址0xc0000000+a肯定映射了该页面。下面轮到了volatile。
     volatile也没有什么大不了的,它其实还没有const重要,volatile仅仅告诉编译器不要试图优化变量,每次访问变量就要访存,而不要自作 聪明的在寄存器缓存。不要指望volatile能提供对共享变量的保护,因此不要随意在内核中用volatile类型的变量。一个变量同时是 volatile和const是可能的,这并不违背常理,前面说过,一个const变量不能被应用程序更改但是却可以被别的实体更改,volatile只是说明不要用缓存到寄存器的值。
最后我们看一下应用程序修改const的情况,首先声明,const关键字之所以存在,旨在提供一个规范,让程序员不要更改const变量的,如果更改了就可能出现大的bug,规范只是规范,如果你不遵守,那么后果就是自负,编译器不会强制你修改的,不是编译器不强制你,而是操作系统的设计使得它根本就做不到,试看下面的代码:

int main( void )

{

    int i =4;

    const int j =3;

    int *p1 = (int *)&i;

    int *p2 = (int *)&j;

    MEMORY_BASIC_INFORMATION mi={0};

    SIZE_T n = VirtualQuery(p1,&mi,sizeof(mi));

    *p1 = 6;   //修改const变量所在的地址指向的内存,实际上就是修改了const变量i

    printf( "value:%d/n", i );

}

i 真的被修改了吗?实际上i真的被修改了,我们的VirtualQuery函数查p1地址的保护属性,查到的mi的Protect字段是0x4,就是 PAGE_READWRITE,这样的话,谁也阻止不了你修改p1指向的内存了,这看起来好像是编译器的失职,但是仔细想想编译器也只能做到这了,内存的页面在操作系统中是按照页面管理的,也就是说系统内部是页面对齐的,一个页面的保护属性是一样的(否则就不在页表项中设置保护属性了,一个页表项管一个页 面)。一个页面典型的是4096字节,而我们的const变量i只有4个字节,非const变量j也是4个字节,且它们都在main函数的栈帧中(栈的空 间十分有限),系统根本不值得也无法专门为const变量开辟4096字节的空间,按照函数调用栈帧的规范,局部变量几乎都是顺序排列的,谁也不比谁特殊,系统不会因为栈帧中有一个const变量就把它所在的整个4096字节的内存全部设置为只读,因此所查到的mi的保护属性是 PAGE_READWRITE。注意后面的打印,打印出来的i依然是4而不是6,这只是编译器以及const的风格而已,const它确保它定义的变量i 的值不会变,而不管i所在的内存是否被修改,这有些拗口,实际上编译器为了依然遵守const不被修改的约定(事实上你修改了),只好将立即数3打印了出 来,立即数3是const变量初始化的时候的值。如果说编译器没有失职,那么是谁的过错呢?实际上是写程序的人的过错(也就是我!),他(我?)这么写说明他根本没有明白c语言的最基本的语法...
上面的例子是const变量在栈中的情形,如果定义成全局变量的话结果是一样的,但是,考虑以下代码:

const int value1 = 3;

int value2 = 4;

int main( void )

{

    int *p1 = (int *)&value1;

    int *p2 = (int *)&value2;

    MEMORY_BASIC_INFORMATION mi={0};

    SIZE_T n = VirtualQuery(p,&mi,sizeof(mi));

    ...

}

猜 猜看p1和p2会差多少,它们的定义是紧挨着呢,按理说地址也应该紧挨着,可是这种情况下它们却没有紧挨着,为什么?因为它们没有在紧缺的栈中,而是处于全局数据区,这里的空间就大了去了,因为value1是const的,那么它占据的地址所在的页面的保护属性都是只读的,再有全局的const变量也将和 value1公用一个页面,而value2不是const的,因此它不能和value1公用一个页面,因此它们的地址相差的不是4而是很多,如果将 value1的const去掉或者将value2加上const,那么它们的地址在大多数情况之下真的就是相差4。再看:

void ChangeConst( int * ip )

{

    *ip=9;

}

int main( void )

{

    const int i =3;

    ChangeConst((int *)&i);

    ...

}

编 译器虽然看得有些失职,但是如果你在调用ChangeConst的时候不把&i(它是const int *)强制转换为int*的话,编译是无法通过的,至于你转换了,出事了(*ip=9),那就谁也别怪了。有一种可贵的精神,如果你想把一件事做成,谁也无 法阻止你,天啊,人家只是一个编译器而已,折磨够了也该休息了。
编写应用程序的时候一定带有内核的思想,反过来也同样,内核的内存粒度是页面而应用的内存粒度却是字节,有时候用户空间所犯下的错误并没有错到陷入内核的地步(比如malloc越界等等,malloc越界后,很多时候并不会马上体现出来,只因内核没有接手错误),那么如何避免隐含的任何bug,只有靠平时 自己的编程习惯,编写安全,健壮的代码。
今天一定陪着老婆看电视...

原文链接: https://blog.csdn.net/dog250/article/details/5303372

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍;

也有高质量的技术群,里面有嵌入式、搜广推等BAT大佬

    char *和char数组的区别(深拷贝和浅拷贝的观点)以及内核访问用户空间

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/410342

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年4月26日 上午11:57
下一篇 2023年4月26日 上午11:58

相关推荐