顯示具有 c pointer 標籤的文章。 顯示所有文章
顯示具有 c pointer 標籤的文章。 顯示所有文章

2021年5月29日 星期六

c 語言傳遞二維陣列給函式時, 應該用什麼型別來接收這個二維陣列

the 1st edition: 2018/5/8
傳送陣列到函式時, 只傳指標過去, 這是什麼意思? 當然是字面上的意思。

char *str[10], 將 str 傳給 function 時, 是傳一個 pointer, 這個 pointer 指向 char*
ref: L38, L39

char str[5][10], 將 str 傳給 function 時, 是傳一個 pointer, 這個 pointer 指向 char [10]
所以一個要用 char** 接, 一個要用 char (*)[] 接
然後 int **argv 和 int*argv[] 是同樣的意思

但是

argv.c 這個範例我故意改用 int *argv[] 而不是 char *argv[], 因為 char *argv[] 又會讓情形更混亂, 所以最後我修改為 int *argv[]。

int *argv[]
int *str[10]

這個很有趣, 根據 *, [] 的優先順序, 這個應該是 int *(str[10]), 也就是 str 應該是 array, 可是 argv 卻不是 array, 而是指標, 不過 int *argv[] 只能寫在函式 prototype 裡頭。

argv.c
 1 #include <stdio.h>
 2 
25 
26 int f(int argc, int (*argv)[])
27 {
28   return 0;
29 }
30 
31 // equal char **argv
32 void f5(int argc, int *argv[])
33 {
34 }
35 
36 int main(int argc, char *argv[])
37 {
38   int *strings[10] = {(int *)0x123, (int *)0x789};
39   f5(2, strings);
40 
41   int str[5][10] = {{1,2,3}, {7,8,9}};
42   f(2, str);
43   return 0;
44 }

2015年7月13日 星期一

有著 const 修飾的 c pointer

經過 os kernel 修煉之後, 我本來以為對指標已達「略懂」的境界, 不過看了《[問題] 為什麼兩個 pointer 不能轉 const》 (該篇的討論有答案) 之後, 我才知道還有不足, 這次不是記憶體佈局的問題, 是在 compiler 這個階段。

有的人說指標很簡單, 我不知道他是真的把指標搞懂了還是誤會了指標的困難度, 指標的複雜度有兩方面: run time 和 compiler time。而這個 const 是在 compiler time, 怎麼把這個 const 指標寫對是很困難的, 而 c 和 c++ 又有不同的觀點。

const int *  *  * p1;
      int *  *  * p2;
p1=p2;   

上述的程式碼無法正常 compile, 我很驚訝,

const int * p1;
      int *p2;
p1=p2;   

這樣就可以, 為什麼多了幾顆星星就不行, 許多人已經提出答案了, 就不重複說明了, 參考以下連結。

ref:
  1. const 的使用 (4) - 基礎測驗解答篇
  2. Re: [問題] 為什麼兩個 pointer 不能轉 const
a.cpp, b.cpp 分別用來觀察在 dereference 時, 取到的型別是什麼?

a.cpp
 1 #include <stdio.h>
 2 #include <typeinfo>
 3 #include <iostream>
 4 
 5 using namespace std;
 6 
 7 void func(const char *const * tokens, int len)
 8 {
 9   for (int i=0 ; i < len ; ++i)
10     cout << *(tokens+i) << endl;
11 }
12 
13 int main(int argc, char *argv[])
14 {
15   char s1[]="abc";
16   char s2[]="xyz";
17   char *tokens[3] = {s1, s2};
18   func(tokens, 2);
19 
20   int *  *  const* p1;
21 
22   cout << "p1 type: " << typeid(p1).name() << endl;
23   cout << "*p1 type: " << typeid(*p1).name() << endl;
24   cout << "**p1 type: " << typeid(**p1).name() << endl;
25   cout << "***p1 type: " << typeid(***p1).name() << endl;
26   return 0;
27 }



result1
p1 type: PKPPi
*p1 type: PPi
**p1 type: Pi
***p1 type: i

PK 是一個 const pointer, P 則是 non-const pointer。

b.cpp
 1 #include <stdio.h>
 2 #include <typeinfo>
 3 #include <iostream>
 4 
 5 using namespace std;
 6 
 7 void func(const char *const * tokens, int len)
 8 {
 9   for (int i=0 ; i < len ; ++i)
10     cout << *(tokens+i) << endl;
11 }
12 
13 int main(int argc, char *argv[])
14 {
15   char s1[]="abc";
16   char s2[]="xyz";
17   char *tokens[3] = {s1, s2};
18   func(tokens, 2);
19 
20   int const*  *  * p1;
21 
22   cout << "p1 type: " << typeid(p1).name() << endl;
23   cout << "*p1 type: " << typeid(*p1).name() << endl;
24   cout << "**p1 type: " << typeid(**p1).name() << endl;
25   cout << "***p1 type: " << typeid(***p1).name() << endl;
26   return 0;
27 }


result2
p1 type: PPPKi
*p1 type: PPKi
**p1 type: PKi
***p1 type: i


手工打造的示意圖
寄件者 ??

再來便是如何正確傳入 const pointer 的問題, 你很想這麼做, 但編譯器老是發出錯誤或是警告訊息嗎? 所以你乾脆不使用 const 了, 我也曾經遇到類似的麻煩事, c.cpp 把幾種情況列出來, c.cpp 無法通過編譯, 只是列出語法, 使用了正確的宣告後, 編譯器終於安靜下來了, 用 c++ compiler 才會安靜, 用 c compiler 一樣會發出警告。請參考 ref 2 的討論。

c.cpp
 1 #include <stdio.h>
 2 #include <typeinfo>
 3 #include <iostream>
 4 
 5 using namespace std;
43   char s1[]="abc";
44   char s2[]="xyz";
45   char s3[]="lmk";
 6 
11 
12 //const char * const tokens[]={s1, s2};
13 void func(const char * const* tokens, int len)
14 {
15   for (int i=0 ; i < len ; ++i)
16     cout << *(tokens+i) << endl;
17 }
18 
19 //const char * tokens[]={s1, s2};
20 void func(const char * * tokens, int len)
21 {
22   for (int i=0 ; i < len ; ++i)
23     cout << *(tokens+i) << endl;
24 }
25 
26 //char * tokens[]={s1, s2};
27 void func(const char * const* tokens, int len)
28 {
29   for (int i=0 ; i < len ; ++i)
30     cout << *(tokens+i) << endl;
31 }
32 
33 //char * tokens[]={s1, s2};
34 void func(char * * tokens, int len)
35 {
36   for (int i=0 ; i < len ; ++i)
37     cout << *(tokens+i) << endl;
38 }

ref:

2014年11月18日 星期二

array and pointer X global and local

我很想知道 g.c 和 g1.c 有什麼不同, source code 之前沒有秘密, 尤其是在組合語言之前。

先來四個問題 ...

Q1: g.c L3 可讀可寫, 為什麼?
Q2: g.c L4 read only, why?

g.c
1 int main(int argc, char *argv[])
2 {
3   char ch[]="abc";
4   const char *chp="abc";
5   return 0;
6 }

Q3: g1.c L1 可讀可寫, 為什麼?
Q4: g1.c L2 read only, why?

g1.c
1   char ch[]="abc";
2   const char *chp="abc";
3 int main(int argc, char *argv[])
4 {
5   ch[0] = '1';
6   return 0;
7 }

先來反組譯 g (source code g.c), L5 把 "abc" 複製到 ch, L6 把 chp 指到 "abc", 這個 "abc" 是為於 .rodata, 所以 chp 指到的內容不能改, chp 本身在 stack。

ch 本身位於 stack, 把 "abc" 複製到這裡自然是可以修改的。

objdump -d g
1 080483cd <main>:
2  80483cd:       55                      push   %ebp
3  80483ce:       89 e5                   mov    %esp,%ebp
4  80483d0:       83 ec 10                sub    $0x10,%esp
5  80483d3:       c7 45 f8 61 62 63 00    movl   $0x636261,-0x8(%ebp)
6  80483da:       c7 45 fc 80 84 04 08    movl   $0x8048480,-0x4(%ebp)
7  80483e1:       b8 00 00 00 00          mov    $0x0,%eax
8  80483e6:       c9                      leave  
9  80483e7:       c3                      ret   

g1呢? 不用反組譯, 看他的 .s 就可以了。ch 和 ch 所指到的 "abc" 在 .data section, 所以可讀寫。 chp 本身位於 .data, 可讀可寫, 但它指到的 "abc" 位於 .rodata, 只能讀取。

gcc -S g1.c
 1  .file "g1.c"
 2  .globl ch
 3  .data
 4  .type ch, @object
 5  .size ch, 4
 6 ch:
 7  .string "abc"
 8  .globl chp
 9  .section .rodata
10 .LC0:
11  .string "abc"
12  .data
13  .align 4
14  .type chp, @object
15  .size chp, 4
16 chp:
17  .long .LC0
18  .text
19  .globl main
20  .type main, @function
21 main:
22 .LFB0:
23  .cfi_startproc
24  pushl %ebp
25  .cfi_def_cfa_offset 8
26  .cfi_offset 5, -8
27  movl %esp, %ebp
28  .cfi_def_cfa_register 5
29  movb $49, ch
30  movl $0, %eax
31  popl %ebp
32  .cfi_restore 5
33  .cfi_def_cfa 4, 4
34  ret
35  .cfi_endproc
36 .LFE0:
37  .size main, .-main
38  .ident "GCC: (Debian 4.8.2-16) 4.8.2"
39  .section .note.GNU-stack,"",@progbits

.rodata section 為什麼會有 read only 的能力, 那是因為 mmu 的作用, 所以若沒有這種記憶體保護機制, 是可以直接修改值的。例如: dos。

就像蒙面魔術師破解魔術一樣, 了解之後, 就知道為什麼可以這樣, 不能那樣。

組合語言的聽說讀寫, 最好能有的能力, 這樣才能了解最底層的秘密。

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 日修訂版) 翻譯而來)
     



2013年5月9日 星期四

c pointer + 1

指標 +1 的時候到底是加了多少呢?書上有寫, 但有時候這個指標的 type 並不是那麼直覺。

address+1.c
 1 #include <stdio.h>
 2 
 3 int main( )
 4 { 
 5   char buf[3];
 6   printf("buf : %p\n",buf);
 7   printf("&buf : %p\n",&buf);
 8   printf("buf+1 : %p\n",buf+1);
 9   printf("(&buf)+1 : %p\n",(&buf)+1);
10   return 0;
11 } 

buf : 0xfff6966d
&buf : 0xfff6966d
buf+1 : 0xfff6966e (加了 1)
(&buf)+1 : 0xfff69670 (加了 3)

雖然 buf, &buf 位址一樣, 但 +1 之後的行為是不一樣的, 一個一樣 +1; 另外一個卻加了整個 array 的大小, 3 bytes。

2013年2月7日 星期四

執行位址 0 的程式碼

ptr.c
 1 int main(int argc, const char *argv[])
 2 {
 3 #if 1
 4   unsigned int addr = 0;
 5   goto *addr; 
5.5  // goto *(0); 
 6 #else
 7   (*(void(*)())0)();
 8 #endif
 9   return 0;
10 }

在 C 陷阱與缺陷 (C Traps and Pitfalls) 上看到的, 令人害怕的語法。 使用 goto 就清楚多了吧!

有興趣的朋友可以更進一步看看反組譯後的組合語言, 很有趣哦!

不過使用 goto 和 function 語意是不同的:


b.c
1 int main(int argc, const char *argv[])
2 {
3   (*(void(*)())(0x22))();
4   goto *(0x20); 
5   return 0;
6 }

objdump -d a.out
 1 08048404 <main>:
 2  8048404:       55                      push   %ebp
 3  8048405:       89 e5                   mov    %esp,%ebp
 4  8048407:       83 e4 f0                and    $0xfffffff0,%esp
 5  804840a:       b8 22 00 00 00          mov    $0x22,%eax
 6  804840f:       ff d0                   call   *%eax
 7  8048411:       b8 20 00 00 00          mov    $0x20,%eax
 8  8048416:       ff e0                   jmp    *%eax
 9  8048418:       90                      nop
10  8048419:       8d b4 26 00 00 00 00    lea    0x0(%esi,%eiz,1),%esi

我實在太大意了。
call, jmp is different.

至於那個 %eiz 是什呢?
ref:
http://blog.csdn.net/unbutun/article/details/6661275

2013年1月14日 星期一

改變指標的內容需要傳指標的指標

L22 mp(p, 5) 應該是很多人的困擾, 為什麼 p 這個指標不會被改成 5 還是原來的 1。

ptrptr.c
 1 /*
 2  *  modify pointer
 3  */
 4 #include <stdio.h>
 5 
 6 void mp(char *ptr, int addr)
 7 {
 8   ptr = addr;
 9   printf("ptr: %p\n", ptr);
10 }
11 


12 void mp1(char **ptrptr, int addr)
13 {
14   *ptrptr = addr;
15   printf("*ptrptr: %p\n", *ptrptr);
16 }
17 
18 int main(int argc, const char *argv[])
19 {
20   char *p=1;
21   printf("p: %p\n", p);
22   mp(p, 5);       
23   printf("p: %p\n", p);
24   mp1(&p, 5);       
25   printf("p: %p\n", p);
26   return 0;
27 }

執行結果:
p: 0x1
ptr: 0x5
p: 0x1
*ptrptr: 0x5
p: 0x5

組合語言面前沒有秘密, 硬著頭皮看看吧!

objdump -d ptrptr
 1 08048404 <mp>:
 2  8048404: 55                    push   %ebp
 3  8048405: 89 e5                 mov    %esp,%ebp
 4  8048407: 83 ec 18              sub    $0x18,%esp
 5  804840a: 8b 45 0c              mov    0xc(%ebp),%eax
 6  804840d: 89 45 08              mov    %eax,0x8(%ebp)
 7  8048410: b8 90 85 04 08        mov    $0x8048590,%eax
 8  8048415: 8b 55 08              mov    0x8(%ebp),%edx
 9  8048418: 89 54 24 04           mov    %edx,0x4(%esp)
10  804841c: 89 04 24              mov    %eax,(%esp)
11  804841f: e8 fc fe ff ff        call   8048320 <printf@plt>
12  8048424: c9                    leave  
13  8048425: c3                    ret    
14 
15 08048426 <mp1>:
16  8048426: 55                    push   %ebp
17  8048427: 89 e5                 mov    %esp,%ebp
18  8048429: 83 ec 18              sub    $0x18,%esp
19  804842c: 8b 55 0c              mov    0xc(%ebp),%edx
20  804842f: 8b 45 08              mov    0x8(%ebp),%eax
21  8048432: 89 10                 mov    %edx,(%eax)
22  8048434: 8b 45 08              mov    0x8(%ebp),%eax
23  8048437: 8b 10                 mov    (%eax),%edx
24  8048439: b8 99 85 04 08        mov    $0x8048599,%eax
25  804843e: 89 54 24 04           mov    %edx,0x4(%esp)
26  8048442: 89 04 24              mov    %eax,(%esp)
27  8048445: e8 d6 fe ff ff        call   8048320 <printf@plt>
28  804844a: c9                    leave  
29  804844b: c3                    ret    
30 
31 0804844c <main>:
32  804844c: 55                    push   %ebp
33  804844d: 89 e5                 mov    %esp,%ebp
34  804844f: 83 e4 f0              and    $0xfffffff0,%esp
35  8048452: 83 ec 20              sub    $0x20,%esp
36  8048455: c7 44 24 1c 01 00 00  movl   $0x1,0x1c(%esp)
37  804845c: 00 
38  804845d: 8b 54 24 1c           mov    0x1c(%esp),%edx
39  8048461: b8 a6 85 04 08        mov    $0x80485a6,%eax
40  8048466: 89 54 24 04           mov    %edx,0x4(%esp)
41  804846a: 89 04 24              mov    %eax,(%esp)
42  804846d: e8 ae fe ff ff        call   8048320 <printf@plt>
43  8048472: 8b 44 24 1c           mov    0x1c(%esp),%eax
44  8048476: c7 44 24 04 05 00 00  movl   $0x5,0x4(%esp)
45  804847d: 00 
46  804847e: 89 04 24              mov    %eax,(%esp)
47  8048481: e8 7e ff ff ff        call   8048404 <mp>
48  8048486: 8b 54 24 1c           mov    0x1c(%esp),%edx
49  804848a: b8 a6 85 04 08        mov    $0x80485a6,%eax
50  804848f: 89 54 24 04           mov    %edx,0x4(%esp)
51  8048493: 89 04 24              mov    %eax,(%esp)
52  8048496: e8 85 fe ff ff        call   8048320 <printf@plt>
53  804849b: c7 44 24 04 05 00 00  movl   $0x5,0x4(%esp)
54  80484a2: 00 
55  80484a3: 8d 44 24 1c           lea    0x1c(%esp),%eax
56  80484a7: 89 04 24              mov    %eax,(%esp)
57  80484aa: e8 77 ff ff ff        call   8048426 <mp1>
58  80484af: 8b 54 24 1c           mov    0x1c(%esp),%edx
59  80484b3: b8 a6 85 04 08        mov    $0x80485a6,%eax
60  80484b8: 89 54 24 04           mov    %edx,0x4(%esp)
61  80484bc: 89 04 24              mov    %eax,(%esp)
62  80484bf: e8 5c fe ff ff        call   8048320 <printf@plt>
63  80484c4: b8 00 00 00 00        mov    $0x0,%eax
64  80484c9: c9                    leave  
65  80484ca: c3                    ret    

L43 0x1c(%esp) 就是 c code 的 char *p=1; L46 會把 *p 也就是 1 push 到 stack。


位址 位址的內容
0x100 p 1




x01000 5
0x1004 1 -> 5
0x1008 return address

call mp() 之後, mp() 修改的是位址 0x1004, 改成 5。對於 main() 的 p 完全沒影響。


L 55, 56 push p address (0x100) 到 stack。

位址 位址的內容
0x100 p 1 -> 5


x01000 5
0x1004 0x100
0x1008 return address

call mp1() 之後, mp1() 修改的是位址 0x100, 改成 5。這就改變了 main() 的 p。

這就是 C 只有 call by value 的說法。

有點難懂的話, 我有個口訣:
「傳入函式要改變變數要傳變數的指標; 要改變指標就要傳指標的指標。」

2012年10月5日 星期五

c pointer vs array and they are different

test environment: 32bit code, in bootloader

array 和 pointer 有什麼不同呢?C 专家编程 (Expert C Programming) 裡頭有不少的說明。我要提到的是另外一個觀察。

cb.c
 1 __asm__(".code16gcc\n");

 5 
 6 void WinMain(void)
 7 {
 8   char arr[]="abcdefghi";
 9   char *arrp = arr;
10   *arrp = 'z';
11 
12   while(1);
13 }

objdump -d -m i8086 cb.elf
 3 cb.elf:     file format elf32-i386
 4 
 5 
 6 Disassembly of section .text:
 7 
 8 00007c00 <WinMain>:
 9     7c00:       66 55                   push   %ebp
10     7c02:       66 89 e5                mov    %esp,%ebp
11     7c05:       66 83 ec 10             sub    $0x10,%esp
12     7c09:       67 66 c7 45 f2 61 62    movl   $0x64636261,-0xe(%ebp)
13     7c10:       63 64 
14     7c12:       67 66 c7 45 f6 65 66    movl   $0x68676665,-0xa(%ebp)
15     7c19:       67 68 
16     7c1b:       67 c7 45 fa 69 00       movw   $0x69,-0x6(%ebp)
17     7c21:       67 66 8d 45 f2          lea    -0xe(%ebp),%eax
18     7c26:       67 66 89 45 fc          mov    %eax,-0x4(%ebp)
19     7c2b:       67 66 8b 45 fc          mov    -0x4(%ebp),%eax
20     7c30:       67 c6 00 7a             movb   $0x7a,(%eax)
21     7c34:       eb fe                   jmp    7c34 <WinMain+0x34>

我透過作業系統之前的程式 (這樣可以簡化), 使用 bochs 內建除錯器 single step 跑了一次, 終於有了深刻的印象。

下圖是我將記憶體的內容 (stack) 根據程式畫出來, 對照著反組譯的 code, 應該很好理解。

17     7c21:       67 66 8d 45 f2          lea    -0xe(%ebp),%eax

把 -0xe(%ebp) 的位址 (ffc4) 抓出來。

arr 本身就代表某個位址, 就是 ffc4, 而在 c 語言用 &arr, arr 都是得到  ffc4。
arrp 本身代表某個位址 (ffce), 而這個位址用來存一個位址 0000ffc4。 &arrp 得到 ffce, arrp 則是 ffc4, 那 *arrp 呢?自然就是從 ffc4 抓一個 byte 的值, 就是 0x61 = 'a'。

%epb = 0xffd2 這是系統的 epb 值, 在不同平台上有可能會不同。

ffd2
00
00
ff
c4ffce -4(%ebp) arrp
00
69ffcc -6(%ebp)
68
67
66
65ffc8 -a(%epb)
64
63
62
61ffc4 -e(%epb) arr

編譯方式:
gcc -fno-stack-protector -std=c99 -m32 -ffreestanding -Wall -g -c cb.c
ld -m elf_i386 -static -Tl.ld -nostdlib -M -o cb.elf cb.o > cb.elf.map
objcopy -R .pdr -R .comment -R.note -S -O binary cb.elf cb.bin

這個簡單的組合語言程式可花了我不少腦力

這是另外一個有趣的問題 (作者: sunlights (sunlights) 看板: C_and_CPP標題: [問題] 指標和陣列的問題)

cb1.c
 1 __asm__(".code16gcc\n");

 8 int main(void)
 9 {
10   char arr[]="abcdefghi";
11   char *arrp = arr;
12   int addr = &arr+1;

22   while(1);
23 }

objdump -d -m i8086 cb.elf
 3 cb.elf:     file format elf32-i386
 4 
 5 
 6 Disassembly of section .text:
 7 
 8 00007c00 <main>:
 9     7c00:       66 55                   push   %ebp
10     7c02:       66 89 e5                mov    %esp,%ebp
11     7c05:       66 83 ec 20             sub    $0x20,%esp
12     7c09:       67 66 c7 45 ee 61 62    movl   $0x64636261,-0x12(%ebp)
13     7c10:       63 64 
14     7c12:       67 66 c7 45 f2 65 66    movl   $0x68676665,-0xe(%ebp)
15     7c19:       67 68 
16     7c1b:       67 c7 45 f6 69 00       movw   $0x69,-0xa(%ebp)
17     7c21:       67 66 8d 45 ee          lea    -0x12(%ebp),%eax
18     7c26:       67 66 89 45 f8          mov    %eax,-0x8(%ebp)
19     7c2b:       67 66 8d 45 ee          lea    -0x12(%ebp),%eax
20     7c30:       66 83 c0 0a             add    $0xa,%eax
21     7c34:       67 66 89 45 fc          mov    %eax,-0x4(%ebp)
22     7c39:       eb fe                   jmp    7c39 <main+0x39>

12   int addr = &arr+1;

會怎樣?
組合語言面前沒有秘密, 似乎就是這行:

20     7c30:       66 83 c0 0a             add    $0xa,%eax

我沒有答案, 為什麼該 C 語言 +1 的動作在組合語言變成 + 0xa, 不懂?

若是改為
&arr+2;
&arr+3;
add    $0x14,%eax
add    $0x1e,%eax 
每加 1, 組合語言則是 + 0xa

難道這是未定義之行為嗎?
推 LPH66:你的 &arr 其型態是 char (*)[10] 因此 +1 時會跳十格        10/05 09:52
→ LPH66:之所以是十格是因為 sizeof(char[10]) == 10 的關係          10/05 09:52
感謝 LPH66, 一語驚醒夢中人。
arr+1
&arr+1
行為很不同, 注意別寫錯了。

2011年12月9日 星期五

c runtime - array vs pointer

這主題不算是 c runtime, 只是我自己想這樣分類。

From wish list books

程式設計師面試寶典 (已經絕版, 連這種書都會絕版, 真讓我驚訝, 這可算是電腦界的高普考書籍耶!): p 7-6 提到的考題:
char c[]="hello";
char *c="hello";
有什麼不同?

書上花了近兩頁的篇幅在解釋, 不過沒有告訴你為什麼?也許你和我一樣, 會想知道為什麼? 看看翻出來的組合語言就知道了。

a.c
1 int main()
2 {
3 char str[]="hello";
4 return 0;
5 }

gcc -S a.c 得到 a.s

9 movl $1819043176, -6(%ebp)
10 movw $111, -2(%ebp)

1819043176 = 0x6C6C6568
111 = 0x6f
"hello" = {0x68, 0x65, 0x6c, 0x6c, 0x6f}
所以第 9, 10 行就是把 hello 放到 %ebp 指的 stack 中。

a.s
1 .file "a.c"
2 .text
3 .globl main
4 .type main, @function
5 main:
6 pushl %ebp
7 movl %esp, %ebp
8 subl $16, %esp
9 movl $1819043176, -6(%ebp)
10 movw $111, -2(%ebp)
11 movl $0, %eax
12 leave
13 ret
14 .size main, .-main
15 .ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
16 .section .note.GNU-stack,"",@progbits
17



a-ro.c
1 int main()
2 {
3 char *str="hello";
4 return 0;
5 }

gcc -S a-ro.c 得到 a-ro.s

2 .section .rodata
3 .LC0:
4 .string "hello"


"hello" 是放在 .rodata section, 這會佔用執行檔案的空間, 所以執行檔會大一些。不過編譯後的執行檔案大小一樣, obj 檔倒是有大小的差異。可能還有我沒搞懂的地方。


a-ro.s
1 .file "a-ro.c"
2 .section .rodata
3 .LC0:
4 .string "hello"
5 .text
6 .globl main
7 .type main, @function
8 main:
9 pushl %ebp
10 movl %esp, %ebp
11 subl $16, %esp
12 movl $.LC0, -4(%ebp)
13 movl $0, %eax
14 leave
15 ret
16 .size main, .-main
17 .ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
18 .section .note.GNU-stack,"",@progbits
19


看來差異不大, 但題目是如果把 c return 回去的話會怎樣?

char c[]="hello";
char *c="hello";
return c;

c[] 放在 stack 裡, 在 return 時, stack 會被清掉不應該這麼描述, 應該說: stack 的位址很多 code 會使用 (ex: call a function), function return 時, 若要存取這個 stack 位址的時候沒有其他 code 來蓋掉這個 stack 位址, 那就不會有問題, 但這是無法保證的。所以無法預期 c[] 的值還會是 "hello", *c 則是在 .rodata, 在執行環境記憶體已經保留出一個位置存放 "hello", 所以沒有問題。

若程式執行起來沒問題, 那是運氣好, 不過, 相信我, 你不會一直有這樣的運氣, 這種 bug 很難找。

至於 *c 存在 .rodata, 所以 *c 指到的 "hello" 是無法修改的, 無法使用 c[0]='z' 來修改。既然唯讀, 也難怪大部份都是看到 const char *c="hello"; 這樣的寫法, 因為我讀過的書說要這樣寫,直覺的寫法就應該是這樣, 現在我知道為什麼了。這也很讓我驚訝, C 是這麼隱晦不清的語言, char *c 從語法來看, 完全看不出來有 const 的意思, 也難怪在 The C Programming Language 之後, 會有那麼多的書籍來討論 C 語言了。

20120312 補充:
完全是我自己搞錯了。
float f=0.1;
int i=1;
f=i;
這通常很容易看出會有問題, 因為 f, i type 是不同的。

char *c="hello" 也是:
"hello" 是 const char pointer
c 是 char pointer,
兩個 type 也是不同, 把他們用 = 在一起, 會發生什麼事情, 自然是寫 code 的人要負責。

在 gcc 的測試下,
char *c="hello";
c[0]='Q';
在執行的時候會得到 Segmentation fault, 因為改變唯讀記憶體的位址, 所以有這樣的行為, 在編譯期間, compiler 沒有抓到這樣的警告, 我有點納悶。當然, 若是用 const char *c="hello";
編譯器就會警告了。

static array vs static pointer


測試讀寫的情形。str[2]='q';
static array 不會放到 .rodata, 所以可以讀寫。

a-static-array.c
1 int main()
2 {
3 //char str[]="hello";
4 static char str[]="hello";
5 str[2]='q';
6 return 0;
7 }


a-static-array.s
1 .file "a.c"
2 .text
3 .globl main
4 .type main, @function
5 main:
6 pushl %ebp
7 movl %esp, %ebp
8 movb $113, str.1247+2
9 movl $0, %eax
10 popl %ebp
11 ret
12 .size main, .-main
13 .data
14 .type str.1247, @object
15 .size str.1247, 6
16 str.1247:
17 .string "hello"
18 .ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
19 .section .note.GNU-stack,"",@progbits
20




pointer 的版本似乎都會將 "hello" 放到 .rodata (不管有沒 static), 所以看來 pointer 的版本應該都只能讀 (執行結果會得到 Segmentation fault), 不能寫。

a-static-pointer.c
1 int main()
2 {
3 //char str[]="hello";
4 static char *str="hello";
5 str[2]='q';
6 return 0;
7 }


a-static-pointer.s
1 .file "a.c"
2 .text
3 .globl main
4 .type main, @function
5 main:
6 pushl %ebp
7 movl %esp, %ebp
8 movl str.1247, %eax
9 addl $2, %eax
10 movb $113, (%eax)
11 movl $0, %eax
12 popl %ebp
13 ret
14 .size main, .-main
15 .section .rodata
16 .LC0:
17 .string "hello"
18 .data
19 .align 4
20 .type str.1247, @object
21 .size str.1247, 4
22 str.1247:
23 .long .LC0
24 .ident "GCC: (Ubuntu 4.4.3-4ubuntu5) 4.4.3"
25 .section .note.GNU-stack,"",@progbits
26


size 差異:
7145 2011-12-09 16:00 a-static-array
7143 2011-12-09 16:00 a-static-pointer

20120117 補充:

在 C 程式語言 (第二版) p C-3 看到 16. 字串常數不再可被更改 ..., 也許就是描述這個情形。
英文版本在 p 260 Strings are no longer modifiable, and so may be placed inread-only memory.