在 Linux Kernel 的程式碼中常常可以看到有趣的語法,像是底下初始化中斷的方式:
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
.handle_irq = handle_bad_irq,
.depth = 1,
.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
}
};
先不去查語法大概可以猜出來這是初始化大小是 NR_IRQS 的struct 陣列,初始化的語法很有趣,方法是 [0 ... NR_IRQS-1] = {} 。
這是 C99 標準的語法 "Designated Initializer",看一個簡化的版本大概就可以猜出來要如何初始化:
int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };
這把前十個初始化為 0, Index從10到99設為2, Index 100設為 3。
雖然C99是 1999年的標準,但應該很少人想到有這樣的語法,雖然 For Loop 也可以辦到同樣的事,但能少打幾行code還是比較開心,但也讓我好奇到底如何做到的。就用下面的程式來看看GCC實際的做法
( 程式可以直接下載)
1 #include <stdio.h>
2
3 int widths_global[] = { [0 ... 9] = 2, [10 ... 99] = 3, [100] = 4 };
4
5 int main(int argc, char* argv[])
6 {
7 int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };
8 int size = sizeof(widths) / sizeof(int);
9 int i;
10
11 for( i = 0; i < size; i++)
12 printf("widths[%d] = %d\n", i, widths[i]);
13 return 0;
14 }
這裡用了兩個 Integer Arrays,一個是全域變數 "widths_global []",另一個是區域變數 "widths []"。照一般作法,已經初始化全域變數會放在 .data section,沒有初始化的全域變數會放在 .bss section,而區域變數會放在 stack 內,等function結束後就會POP out 區域變數。全域變數 "widths_global" 應該是compiler在編譯時就填好初始值到 .data section。而區域變數 "widths" 到底是用什麼方式初始化?是用for loop嗎?
直接編譯程式然後反組譯看看
(反組譯後的檔案可以直接下載)
$ gcc t_designated_initializers.c -g -O0
參數 -g是加入 debug 資訊參數 -O0 是不要最佳化
$ objdump -D -s -S a.out > objdump.txt
參數-D: 反組譯所有內容
-s : 所有section內的內容
-S: 加入source codes
反組譯 .data section的內容如下:
143 Contents of section .data:
144 107f4 00000000 00000000 02000000 02000000 ................
145 10804 02000000 02000000 02000000 02000000 ................
146 10814 02000000 02000000 02000000 02000000 ................
147 10824 03000000 03000000 03000000 03000000 ................
148 10834 03000000 03000000 03000000 03000000 ................
149 10844 03000000 03000000 03000000 03000000 ................
150 10854 03000000 03000000 03000000 03000000 ................
151 10864 03000000 03000000 03000000 03000000 ................
152 10874 03000000 03000000 03000000 03000000 ................
153 10884 03000000 03000000 03000000 03000000 ................
154 10894 03000000 03000000 03000000 03000000 ................
155 108a4 03000000 03000000 03000000 03000000 ................
156 108b4 03000000 03000000 03000000 03000000 ................
157 108c4 03000000 03000000 03000000 03000000 ................
158 108d4 03000000 03000000 03000000 03000000 ................
159 108e4 03000000 03000000 03000000 03000000 ................
160 108f4 03000000 03000000 03000000 03000000 ................
161 10904 03000000 03000000 03000000 03000000 ................
162 10914 03000000 03000000 03000000 03000000 ................
163 10924 03000000 03000000 03000000 03000000 ................
164 10934 03000000 03000000 03000000 03000000 ................
165 10944 03000000 03000000 03000000 03000000 ................
166 10954 03000000 03000000 03000000 03000000 ................
167 10964 03000000 03000000 03000000 03000000 ................
168 10974 03000000 03000000 03000000 03000000 ................
169 10984 03000000 03000000 04000000 ............
可以看出來 widths_global[] 就放在 0x107fc -- 0x1098c 內,符合當初預期。但是 widths[]呢,這就有點複雜了,需要看使用時的code,所以要看 main的組合語言。反組譯 main()的內容如下:
487 int main(int argc, char* argv[])
488 {
489 8400: e92d4800 push {fp, lr}
490 8404: e28db004 add fp, sp, #4
491 8408: e24ddf6a sub sp, sp, #424 ; 0x1a8
492 840c: e50b01a8 str r0, [fp, #-424] ; 0x1a8
493 8410: e50b11ac str r1, [fp, #-428] ; 0x1ac
494 int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };
495 8414: e59f3088 ldr r3, [pc, #136] ; 84a4 <main+0xa4>
496 8418: e24b1e1a sub r1, fp, #416 ; 0x1a0
497 841c: e1a02003 mov r2, r3
498 8420: e3a03f65 mov r3, #404 ; 0x194
499 8424: e1a00001 mov r0, r1
500 8428: e1a01002 mov r1, r2
501 842c: e1a02003 mov r2, r3
502 8430: ebffffbb bl 8324 <memcpy@plt>
503 int size = sizeof(widths) / sizeof(int);
504 8434: e3a03065 mov r3, #101 ; 0x65
505 8438: e50b300c str r3, [fp, #-12]
506 int i;
507
508 for( i = 0; i < size; i++)
509 843c: e3a03000 mov r3, #0
510 8440: e50b3008 str r3, [fp, #-8]
511 8444: ea00000e b 8484 <main+0x84>
512 printf("widths[%d] = %d\n", i, widths[i]);
513 8448: e59f2058 ldr r2, [pc, #88] ; 84a8 <main+0xa8>
514 844c: e51b1008 ldr r1, [fp, #-8]
515 8450: e59f3054 ldr r3, [pc, #84] ; 84ac <main+0xac>
516 8454: e1a01101 lsl r1, r1, #2
517 8458: e24b0004 sub r0, fp, #4
518 845c: e0801001 add r1, r0, r1
519 8460: e0813003 add r3, r1, r3
520 8464: e5933000 ldr r3, [r3]
521 8468: e1a00002 mov r0, r2
522 846c: e51b1008 ldr r1, [fp, #-8]
523 8470: e1a02003 mov r2, r3
524 8474: ebffffa7 bl 8318 <printf@plt>
525 {
526 int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };
527 int size = sizeof(widths) / sizeof(int);
528 int i;
529
530 for( i = 0; i < size; i++)
531 8478: e51b3008 ldr r3, [fp, #-8]
532 847c: e2833001 add r3, r3, #1
533 8480: e50b3008 str r3, [fp, #-8]
534 8484: e51b2008 ldr r2, [fp, #-8]
535 8488: e51b300c ldr r3, [fp, #-12]
536 848c: e1520003 cmp r2, r3
537 8490: baffffec blt 8448 <main+0x48>
538 printf("widths[%d] = %d\n", i, widths[i]);
539 return 0;
540 8494: e3a03000 mov r3, #0
541 }
542 8498: e1a00003 mov r0, r3
543 849c: e24bd004 sub sp, fp, #4
544 84a0: e8bd8800 pop {fp, pc}
545 84a4: 00008534 andeq r8, r0, r4, lsr r5
546 84a8: 00008520 andeq r8, r0, r0, lsr #10
547 84ac: fffffe64 ; <UNDEFINED> instruction: 0xfffffe64
恩... 程式碼挺長的,先看前面幾行,Line 491會配置420 bytes的區域變數,不過程式碼內只有 int widths[101],int size,int i 這三個變數,加起來是 (101 + 1 + 1 ) * 4 = 412,剩下的8 bytes是什麼? 從Line 492 ~ 493 可以看出來是用來儲存 r0 還有 r1。
487 int main(int argc, char* argv[])
488 {
489 8400: e92d4800 push {fp, lr}
490 8404: e28db004 add fp, sp, #4 => fp = sp + 4
491 8408: e24ddf6a sub sp, sp, #424 ; 0x1a8 => 配置 420 bytes的
492 840c: e50b01a8 str r0, [fp, #-424] ; 0x1a8
493 8410: e50b11ac str r1, [fp, #-428] ; 0x1ac
494 int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };
可以整理出目前的 Stack內容如下
接著繼續往下看,line 495是把 0x84a4的內容放到r3內
495 8414: e59f3088 ldr r3, [pc, #136] ; 84a4 <main+0xa4>
496 8418: e24b1e1a sub r1, fp, #416 ; 0x1a0
497 841c: e1a02003 mov r2, r3
498 8420: e3a03f65 mov r3, #404 ; 0x194
499 8424: e1a00001 mov r0, r1
500 8428: e1a01002 mov r1, r2
501 842c: e1a02003 mov r2, r3
502 8430: ebffffbb bl 8324 <memcpy@plt>
0x84a4的內容是 0x8534,看起來是沒有什麼意義的東西,也許是一個記憶體位置。
545 84a4: 00008534
這就要看如何使用他了,r3 藉著會被搬到 r2,然後在被搬到 r1,最後會在 line 502呼叫 memcpy。所以 0x8534會被當成 memcpy的第二個參數。查詢一下 memcpy()的參數。
$ man memcpy
MEMCPY(3) Linux Programmer's Manual MEMCPY(3)
NAME
memcpy - copy memory area
SYNOPSIS
#include <string.h>
void *memcpy(void *dest, const void *src, size_t n);
DESCRIPTION
The memcpy() function copies n bytes from memory area src to memory
area dest. The memory areas must not overlap. Use memmove(3) if the
memory areas do overlap.
這樣可以知道第二個參數是 src,是指向記憶體的指標,這樣就確定 0x8534是記憶體位置了,接著看看內容。
85 Contents of section .rodata:
86 851c 01000200 77696474 68735b25 645d203d ....widths[%d] =
87 852c 2025640a 00000000 01000000 01000000 %d.............
88 853c 01000000 01000000 01000000 01000000 ................
89 854c 01000000 01000000 01000000 01000000 ................
90 855c 02000000 02000000 02000000 02000000 ................
91 856c 02000000 02000000 02000000 02000000 ................
92 857c 02000000 02000000 02000000 02000000 ................
93 858c 02000000 02000000 02000000 02000000 ................
94 859c 02000000 02000000 02000000 02000000 ................
95 85ac 02000000 02000000 02000000 02000000 ................
96 85bc 02000000 02000000 02000000 02000000 ................
97 85cc 02000000 02000000 02000000 02000000 ................
98 85dc 02000000 02000000 02000000 02000000 ................
99 85ec 02000000 02000000 02000000 02000000 ................
100 85fc 02000000 02000000 02000000 02000000 ................
101 860c 02000000 02000000 02000000 02000000 ................
102 861c 02000000 02000000 02000000 02000000 ................
103 862c 02000000 02000000 02000000 02000000 ................
104 863c 02000000 02000000 02000000 02000000 ................
105 864c 02000000 02000000 02000000 02000000 ................
106 865c 02000000 02000000 02000000 02000000 ................
107 866c 02000000 02000000 02000000 02000000 ................
108 867c 02000000 02000000 02000000 02000000 ................
109 868c 02000000 02000000 02000000 02000000 ................
110 869c 02000000 02000000 02000000 02000000 ................
111 86ac 02000000 02000000 02000000 02000000 ................
112 86bc 02000000 02000000 03000000 ............
看起來就是 widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 }; 的內容。接著看 memcpy()的另外兩個參數 r0 = fp - d'416 = (old sp + 4) - d'416 = old sp - 412。雖然 412 / 4 = 103,看起來有點奇怪,但是前8 bytes其實是另外兩個區域變數 int i 還有 int size,很有可能是這兩個區域變數。
另外 r1是 404 , 也就是 101 * 4 = 404,所以可以很確認 widths[ ] 是透過 memcpy() 去初始化,而非用 for loop。
可以整理出目前的Stack
Stack裡面還有兩個記憶體不知道存什麼,所以要繼續看下去
503 int size = sizeof(widths) / sizeof(int);
504 8434: e3a03065 mov r3, #101 ; 0x65
505 8438: e50b300c str r3, [fp, #-12] => (old sp + 4) - 12 = old sp - 8
506 int i;
507
508 for( i = 0; i < size; i++)
509 843c: e3a03000 mov r3, #0
510 8440: e50b3008 str r3, [fp, #-8] => (old sp + 4) - 8 = old sp - 4
511 8444: ea00000e b 8484 <main+0x84>
Line 504, 505可以看出來 old sp - 8 存放值 101,這是 int size = sizeof(widths) / size(int) = 404/ 4 = 0。
Line 509, 510可以看出來 old sp -4 存放值 0,這是 int i = 0。
所以 Stack可以整理出來如下:
結論:
全域變數widths_global[] 會放到 .data section,而且是在編譯階段就填好值,不需在執行時另外花費計算量。區域變數widths[],除了會耗費 stack size外,還會使用到 .rodata section去儲存初始值。也就是瞬間會用到兩倍widths[]大小的記憶體。