2014年5月5日 星期一

聊聊 c null/0 pointer

the 1st editon: 20120829

好久之前寫的文章, 再不發表可能就不會發表了, 寫技術文章真的很累人。

ref 2 提到, null pointer 和 0 pointer 是不一樣的, 但 c 似乎把 0 pointer 當 NULL pointer 來用。c++ 11 提供了 nullptr 這個 keyword 來表示 NULL pointer。

int main(void)
{
  char *p=nullptr;
  *p='a';
}

我們來看看 cb.c 這個對位址 0 做寫入動作的程式碼。

cb.c
 1 __asm__(".code16gcc\n");
 2 
 3 void func(char c, int i, const char *ptr);
 4 
 5 void c_main(void)
 6 {
 7   __asm__ ("mov $0, %ax");
 8   __asm__ ("mov %ax, %ss");
 9   __asm__ ("mov %ax, %ds");
10   char *p=0;
11   *p='a';
12   __asm__ ("mov %cs, %ax");
13   __asm__ ("mov %ax, %ss");
14   __asm__ ("mov %ax, %ds");
15   //while(1);
16 }
17 
18 void func(char c, int i, const char *ptr)
19 {
20   c+=3;
21   i+=2;
22   ++ptr;
23   #if 0
24   char ch;
25   int stack_int;
26   ch = 'b';
27   stack_int = 8;
28   #endif
29 }

看到 L10, L11 大概有人要笑我連基本指標概念都沒有, 0 指標是不能 assign 值的。我的確不是很了解 c 語言的指標, 而我總是在想著, 位址 0 不也是一個位址嗎?哪有不可存取的道理, 其中有著什麼魔法呢?

在 gcc 中 NULL 被 define to (void *)0
char *p=((void *)0);

你和我有相同的疑問嗎?
L10, L11 在 linux 環境下執行, 沒有意外, 得到了 Segmentation fault。

而在 dos 環境下, 得到如下的結果。



9257 (57 92) 是絕對位址 0 的內容, 從 debug 可以看出 (5792), 執行 cb 得到 9257 (57 92), 9261 (61 92) (螢幕最上方的紅色字) 兩個的結果,

這是在

10   char *p=0;
11   *p='a';

前後印出的結果。用 debug 再看一次, 可以得到一樣的結果, 證明絕對位址 0 被改變了。0 pointer 不在是神祕的指標, 它就和一般指標一樣。在 ms dos 下, 絕對位址 0 紀錄某個中斷服務程式的位址。 

那為什麼在 linux 下有問題呢?

自然是 linux 把 0 位址做了手腳, 發出個拒絕存取的舉動。這部份我還在研究, 暫時無法提供程式碼的例子。大概就是設定 mmu, 將 0 這個位址做了禁止讀寫的動作, 只要一讀寫 0, 就會發出一個 exception。

這裡有個類似的問題:
http://programmers.stackexchange.com/questions/147713/where-are-null-values-stored-or-are-they-stored-at-all

c_init.S 就是用來印出這兩個位址 0 的值。

c_init.S
  2 
  3 #define DOS
  4 

 37 
 38 
 39 .text
 40   jmp _start
 41 .global _start
 42 _start:
 43   xchg %bx, %bx
 44 #if 1
 45   mov %cs, %ax
 46   mov %ax, %ds
 47   mov %ax, %ss
 48 
 49 #endif

 51 
 52   #calll _Z9print_strPKc
 53 

 59   mov $0xb800, %ax
 60   mov %ax, %gs
 61 
 62   mov $0, %ax
 63   mov %ax, %fs
 64 
 65   mov %fs:0, %bx
 66   calll disp_bx
 67 
 68   call init_bss_asm # in dos need not init bss by myself
 69 
 70 
 71   calll c_main
 72 
 73 
 74   mov %fs:0, %bx
 75   calll disp_bx

 77   mov     $0x4c00, %ax
 78   int     $0x21   # 回到 DOS
 79 

100 

102 
103 # init bss
104 init_bss_asm:
105   movw $__bss_end__, %di    /* Destination */
106   movw $__bss_start__, %si   /* Source */
107   movw %ds, %bx
108   movw %bx, %es
109   jmp 2f
110 1:
111   mov $0, %eax
112   movw %si, %ax
113   movb $0x0, %es:(%eax)
114   add $1, %si

121   
122 2:
123   cmpw %di, %si
124   jne 1b
125 
126   ret
127 


source code:
simple os git commit: 5fa08df042d7404a4eb147835c0a048052c3ebbf
simple_os/c_runtime

順便提一下 c++, c++ 11 有 nullptr 這個 keyword, 不過在 g++ 4.7 測試下:


nullptr.cpp
 1 /*
 2  * c++11 nullptr test
 3  */
 4 __asm__(".code16gcc\n");
 5 #include "io.h"
 6 #include "obj.h"
 7 
 8 typedef signed char s8;
 9 typedef signed short s16;
10 typedef signed int s32;
11 
12 typedef unsigned char u8;
13 typedef unsigned short u16;
14 typedef unsigned int u32;
15 

22 
23 
24 #define BOCHS_MB __asm__ __volatile__("xchg %bx, %bx");
25 
26 extern "C" int cpp_main(void)
27 {
28   __asm__ ("mov $0, %ax");
29   __asm__ ("mov %ax, %ss");
30   __asm__ ("mov %ax, %ds");
31   //char *p=0;
32   char *p=nullptr;
33   *p='a';
34   __asm__ ("mov %cs, %ax");
35   __asm__ ("mov %ax, %ss");
36   __asm__ ("mov %ax, %ds");
37 
38   return 0;
39 }

31   //char *p=0;
32   char *p=nullptr;

這兩行沒有差別, 都是翻成 char *p=0;


a.d
 1 000001e0 <cpp_main>:
 2  1e0:   66 55                   push   %ebp
 3  1e2:   66 89 e5                mov    %esp,%ebp
 4  1e5:   66 83 ec 10             sub    $0x10,%esp
 5  1e9:   b8 00 00                mov    $0x0,%ax
 6  1ec:   8e d0                   mov    %ax,%ss
 7  1ee:   8e d8                   mov    %ax,%ds
 8  1f0:   67 66 c7 45 fc 00 00    addr32 movl $0x0,-0x4(%ebp)
 9  1f7:   00 00 
10  1f9:   67 66 8b 45 fc          addr32 mov -0x4(%ebp),%eax
11  1fe:   67 c6 00 61             addr32 movb $0x61,(%eax)
12  202:   8c c8                   mov    %cs,%ax
13  204:   8e d0                   mov    %ax,%ss
14  206:   8e d8                   mov    %ax,%ds
15  208:   66 b8 00 00 00 00       mov    $0x0,%eax
16  20e:   66 c9                   leavel 
17  210:   66 c3                   retl   

而在 x86 要存取位址 0 比想像中還要麻煩, 由於獨特的 segment 定址模式, cb.c L7,8,9 就是為了要存取絕對位址 0, 否則 L10 的 0 並不是真正的 0 而是 ds:0, ds 若不是 0, 那就不會存取到絕對位址 0, 真麻煩, 你得在 flat mode 才有這樣使用的機會, 怎麼設定呢? 需要進入 x86 保護模式才行。

那 coretex m3 可以嗎? 很遺憾, 雖然在 cm3 平台上 L10 的確代表著絕對位址 0, 但該位址是 flash 的位址 0, 無法寫入, 也許有方法可以改變位址的 mapping, 不過我沒研究就是, 怎麼樣, 想要直接對位址 0 寫入還沒那麼容易吧!

ref:
  1. 系統程式員成長計劃 (簡體中文版本) p35
  2. A zero pointer is not a null pointer: http://lwn.net/Articles/342558/
  3. 6.14 說真的, 真有機器用非零空指針嗎, 或者不同類型用不同的表達?(本文從英文 C-FAQ (2004 年 7 月 3 日修訂版) 翻譯而來)
     



沒有留言:

張貼留言

使用 google 的 reCAPTCHA 驗證碼, 總算可以輕鬆留言了。

我實在受不了 spam 了, 又不想讓大家的眼睛花掉, 只好放棄匿名留言。這是沒辦法中的辦法了。留言的朋友需要有 google 帳號。