1. 程式人生 > >char *和char陣列的區別(深拷貝和淺拷貝的觀點)以及核心訪問使用者空間

char *和char陣列的區別(深拷貝和淺拷貝的觀點)以及核心訪問使用者空間

From :  http://blog.csdn.net/dog250/article/details/5303372

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的引用計數

}

這兩個關鍵字是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);

    ...

}