오리뎅이의 네떡세상

[오리뎅이의 C 포인터 이야기 - 1] 포인터를 쉽게 배울수 있을까요?

오리뎅이의 C 포인터 이야기

안녕하세요? 오리뎅이입니다.

 

 

C를 포기하는 가장 큰 이유가 포인터 때문이라는 얘기를 많이 들었습니다. 포인터 난관을 뚫고 C 프로그래밍을 잘 하시는 분들은 포인터를 너무 좋아하고 잘 다루시는데요. 알고 나면, 쉬운게 왜 처음에 배우는 게 그리 어려운 것일까? 쉽게 배울 수는 없을까? 그 해답을 찾기 위해서 제 스스로 카페에 올라오는 포인터에 대한 수많은 종류의 질문들을 보면서 무엇을 어려워 하는지 연구하고, 포인터에 대한 칼럼이나 포인터를 다룬 교재들에서 어떻게 가르치고 있는지를 보면서 왜 배우기가 어려운 건지를 생각해 보면서 쉽게 터득할 수 있도록 가르치는 방법을 고민했습니다.

 

이거슨 순전히 포포자(포인터 때문에 C 포기한 자 ㅠ.ㅠ)분들과 이제 C를 시작하시고 포인터를 막 시작하신 분들을 독자로 생각하고 쓰는 이야기입니다. 이미 포인터 산맥을 넘으신 분들이 혹시라도 이 글을 보시게 된다면, 틀린 얘기 하고 있는 거 없는지? 정말로 이렇게 가르치면 초보자나 포포자 분들이 포인터 난관을 쉽게 헤쳐 나갈 수 있을 거 같은지? 살펴 봐 주시고, 잘 못된 부분에 대한 지적이나 더 나은 방법에 대한 조언을 해 주시면 좋겠습니다. 

 

C 프로그래밍 memory layout

포인터를 공부하기에 앞서서 기초 체력을 키우기 위해 꼭 필요한 사전 지식 몇가지를 먼저 알아 보려고 합니다. 첫번째로 "메모리 레이아웃"입니다. 

 

"포인터는 메모리 주소다"

"Pointer is an address of memory"

 

뒤에서 좀 더 자세히 다루겠지만, 포인터는 메모리 주소입니다. 메모리를 이해 하는 것이 포인터를 쉽게 이해하기 위한 첫번째 기초 체력입니다. C 언어의 메모리 레이아웃에 대해서 정리한 글을 몇개 찾아서 아래에 링크를 걸어 놓았습니다. 각 링크 한번씩 방문하셔서 쓰윽 훑어 봐 주세요. 광고 아닙니다. ^^.  어려운 내용들이기때문에 내용을 다 이해하시면서 읽으실 필요는 없습니다. 포인터 공부를 하기 위한 기초체력을 쌓기 위한 것이라서 변수들이 메모리에 이렇게 배치가 되는 구나!! 정도만 살짝 알아 주시면 됩니다. 

 

https://gusdnd852.tistory.com/16

 

C언어 메모리 세그먼트

C를 공부하기 전에 C언어의 메모리 구조를 알면 C언어를 이해하는데 큰 도움이 되며, C이외에도 Java 등의 언어도 이러한 구조와 굉장히 비슷하게 설계되어있다. (아주 약간의 차이만 존재한다) 때

gusdnd852.tistory.com

 

blog.naver.com/justkukaro/220681279377

 

19-C언어:메모리 레이아웃

[[목차]]1.메모리 레이아웃2.프로그램 코드3.전역,정적 자료4.스택5.힙6.마치며 1.메모리 레이아웃 <출...

blog.naver.com

 

https://bigpel66.tistory.com/9

 

[씹어먹는 C 언어 리뷰] Process Memory Layout

많은 사람들이 C 언어를 배우면서 동적할당이라는 것을 배우다 보면 Heap이라는 메모리 영역에 대해서 듣게 되고, 이에 따라 프로세스를 실행했을 때 할당되는 메모리 구조를 배우게 될 것이다.

bigpel66.tistory.com

 

그림1. C 언어 메모리 레이아웃 참조 그림

그림1의 왼쪽 그림과 오른쪽 그림은 닮았지만, 조금 다른 부분이 있어서 함께 가져와 봤습니다. 저는 오른쪽 그림이 더 마음에 들기는 하는데요. 왼쪽 그림에 .init, .text, .rodata 이 부분이 있어서 활용하려고 함께 참조했습니다. 그런데, 두번째 링크와 세번째 링크에서 .init, .roddata를 설명하는 부분이 제가 알던 내용이랑 조금 달랐습니다.

 

"백문이 불여일타"

예전에 프로그래밍 공부할 때, 이 말을 자주 했었습니다. 이론적인 내용 백번 보고 듣고 하느니보다는 한번 내 손끝으로 타이핑해서 직접 돌려 보는게 확실하게 똬악 느낌이 올 때가 많습니다. 

 

변수가 메모리의 어느 위치에 배치되는 지를 확인해 보기 위해서 각각의 변수를 선언하고, 변수의 주소를 출력해 보고자 합니다. 변수 앞에 단항 연산자 &(ampersand)를 붙이면, 연산 결과는 변수가 배치되어 있는 메모리의 주소가 됩니다. printf 문에서 출력 포맷 %p는 주소 출력에 사용하는 형식입니다. %p를 사용하면 주소를 0x로 시작하는 hex value 로 출력해 줍니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#include <stdio.h>
#include <stdlib.h>
 
int globalNoInitialization;                            // BSS, Block Started by Simbol
int globalInitialization = 10;                         // Data, initialized
 
int const globalConstantInitialization = 20;           // Text, .rodata
char* constantGlobalString = "constant global string"// Text, .rodata
 
int stackFunction(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int a10)
{
 
    static int repeate = 0;
 
    printf("Repeate = %d\n\n", repeate + 1);           // printf string " " 부분, .rodata
 
    printf("&a1 local function param      :Stack    = %p\n"&a1);
    printf("&a2 local function param      :Stack    = %p\n"&a2);
    printf("&a3 local function param      :Stack    = %p\n"&a3);
    printf("&a4 local function param      :Stack    = %p\n"&a4);
    printf("&a5 local function param      :Stack    = %p\n"&a5);
    printf("&a6 local function param      :Stack    = %p\n"&a6);
 
    printf("\n");     // Linux에서는 인자를 스택으로 6개까지만 전달한다. 6개 이후는 caller Local Stack
 
    printf("&a7 local function param      :Stack    = %p\n"&a7);
    printf("&a8 local function param      :Stack    = %p\n"&a8);
    printf("&a9 local function param      :Stack    = %p\n"&a9);
    printf("&a10 local function param     :Stack    = %p\n"&a10);
 
    printf("\n");
 
    repeate++;
 
    if (repeate >= 3)
        return 0;
 
    stackFunction(12345678910);
 
}
 
int main(void) {
    static int  staticNoInitialization;                  // BSS, Block Started by Simbol
    static int  staticInitialization = 30;               // Data, initialized
 
    static int const statiConstantInitialization = 40;   // Text, .rodata
    char* constantLocalString = "constant Local string"// Text, .rodata
 
    int stackNoInitialization;
    int stackInitialization01 = 50;
    int stackInitialization02 = 60;
    int stackInitialization03 = 70;
    int stackInitialization04 = 80;
 
    int* stackMallocIntPointer;
    char* stackMallocCharPointer;
    long* stackMallocLongPointer;
 
    stackMallocIntPointer = (int*)malloc(sizeof(int));            // Heap
    stackMallocCharPointer = (char*)malloc(sizeof(char) * 100);   // Heap
    stackMallocLongPointer = (long*)malloc(sizeof(long));         // Heap
 
    globalNoInitialization = globalInitialization;
    staticNoInitialization = staticInitialization;
 
 
    printf("&constantLocalString          :Stack  = %p\n"&constantLocalString);
    printf("&stackNoInitialization        :Stack  = %p\n"&stackNoInitialization);
    printf("&stackInitialization01        :Stack  = %p\n"&stackInitialization01);
    printf("&stackInitialization02        :Stack  = %p\n"&stackInitialization02);
    printf("&stackInitialization03        :Stack  = %p\n"&stackInitialization03);
    printf("&stackInitialization04        :Stack  = %p\n"&stackInitialization04);
    printf("&stackMallocIntPointer        :Stack  = %p\n"&stackMallocIntPointer);
    printf("&stackMallocCharPointer       :Stack  = %p\n"&stackMallocCharPointer);
    printf("&stackMallocLongPointer       :Stack  = %p\n"&stackMallocLongPointer);
 
    printf("\n");
 
    printf("stackMallocIntPointer         :Heap   = %p\n", stackMallocIntPointer);
    printf("stackMallocCharPointer        :Heap   = %p\n", stackMallocCharPointer);
    printf("stackMallocLongPointer        :Heap   = %p\n", stackMallocLongPointer);
 
    printf("\n");
 
    printf("&globalNoInitialization       :BSS    = %p\n"&globalNoInitialization);
    printf("&staticNoInitialization       :BSS    = %p\n"&staticNoInitialization);
 
    printf("\n");
 
    printf("&globalInitialization         :Data   = %p\n"&globalInitialization);
    printf("&staticInitialization         :Data   = %p\n"&staticInitialization);
 
    printf("\n");
 
    printf("&globalConstantInitialization :Text   = %p\n"&globalConstantInitialization);
    printf("constantGlobalString          :Text   = %p\n", constantGlobalString);
    printf("&statiConstantInitialization  :Text   = %p\n"&statiConstantInitialization);
    printf("constantLocalString           :Text   = %p\n", constantLocalString);
    printf("main                          :Text   = %p\n", main);
    printf("stackFunction                 :Text   = %p\n", stackFunction);
 
    printf("\n");
 
    stackFunction(12345678910);
 
    return 0;
}
cs

참조1. C 메모리 레이아웃 이해를 돕기 위한 예제 코드

 

코드를 복사해서 독자분들의 리눅스 머신에 옮겨서 컴파일하고 실행을 시켜 보세요. 제 Virtual Box 우분투 머신에서 돌려본 결과는 다음와 같습니다. Windows나 MAC 등 다른 OS는 같은 C라도 메모리 레이아웃이 다를 수 있습니다. 이거슨 Linux에서 실행한 결과라는 것 알아 주세요. ^^. 우선 참조1에서 104번 stackFunction 실행 결과를 제외하고, 그 앞부분까지의 결과입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
root@k1-duck01:~/limcs/c# gcc -o mem.o mem_laylout.c     // 컴파일
root@k1-duck01:~/limcs/c# ./mem.o                        // 
&constantLocalString          :Stack  = 0x7fffec161208     // Stack, 아래로 자람
&stackNoInitialization        :Stack  = 0x7fffec1611f4     // Stack, 아래로 자람
&stackInitialization01        :Stack  = 0x7fffec1611f8     // Stack, 아래로 자람
&stackInitialization02        :Stack  = 0x7fffec1611fc     // Stack, 아래로 자람
&stackInitialization03        :Stack  = 0x7fffec161200     // Stack, 아래로 자람
&stackInitialization04        :Stack  = 0x7fffec161204     // Stack, 아래로 자람
&stackMallocIntPointer        :Stack  = 0x7fffec161210     // Stack, 아래로 자람
&stackMallocCharPointer       :Stack  = 0x7fffec161218     // Stack, 아래로 자람
&stackMallocLongPointer       :Stack  = 0x7fffec161220     // Stack, 아래로 자람
 
stackMallocIntPointer         :Heap   = 0x555d79b4e2a0      // Heap, 위로 자람 
stackMallocCharPointer        :Heap   = 0x555d79b4e2c0      // Heap, 위로 자람
stackMallocLongPointer        :Heap   = 0x555d79b4e330      // Heap, 위로 자람
 
&globalNoInitialization       :BSS    = 0x555d78d1102c      // bss, block started by symbol
&staticNoInitialization       :BSS    = 0x555d78d11028      // bss, block started by symbol
 
&globalInitialization         :Data   = 0x555d78d11010      // rwdata, read/write data
&staticInitialization         :Data   = 0x555d78d11014      // rwdata, read/write data
 
&globalConstantInitialization :Text   = 0x555d78d0f008      // rodata, read only data
constantGlobalString          :Text   = 0x555d78d0f00c      // rodata, read only data
&statiConstantInitialization  :Text   = 0x555d78d0f64c      // rodata, read only data
constantLocalString           :Text   = 0x555d78d0f216      // rodata, read only data
 
main                          :Text   = 0x555d78d0e33a      // code
stackFunction                 :Text   = 0x555d78d0e1a9      // code
 
cs

참조2. C 프로그래밍 memory out 시험 코드 출력 결과 

 

이해를 돕기 위해서 메모리 주소 출력 순서를 그림1과 같은 순서가 되도록 변수들을 아래쪽에 낮은 번지가 오고, 윗쪽에 높은 번지가 오도록 배치해서 출력했습니다. 출력결과와 코드를 하나씩 보면서 어떤 변수가 어느 위치에 배치되어 출력되는지 보세요. 순서적으로 보았을 때, 실제로 C 프로그레밍 메모리 레이아웃 설명 결과와 부합하는 위치에 있는지 살펴 보세요. Linux에서 돌려 보시고, Windows에서도 돌려 보세요. macOS에서도 돌려 보시고 비교해 보세요. 참고로 Windows 10에서는 Stack보다 Heap 더 높은 메모리 위치에 옵니다. Linux와 서로 위치가 뒤바뀐 위치에요.

 

이 예제 코드 뿐만이 아니라 변수들 메모리 레이아웃이 이해가 쏙 될때까지 수시로 잘 모르겠다 싶은 변수들이 생기면, 그 즉시 즉시 출력 코드를 만들어서 찍어 보시기를 강력히 권장 드립니다. ^^

 

Text (.init, .text, .rodata)

맨 밑부분에 보변 main 함수와 stackFuction 함수 주소가 출력되었습니다. Code 부분 함수들이 화일에서 함수의 위치가 윗부분에 있는 것부터 낮은 주소 영역에 촥촥촥 배치됩니다. .text section 영역에 code들이 배치됩니다. code 위로 .rodata 가 보입니다. 초기화가 된 global/static const 변수들상수 문자열, printf의 문자열 부분 등이 배치 됩니다.  모두 read only 영역 이기때문에 이 곳에 write를 하려고 하면 segmentation fault로 프로세스가 뻗게 됩니다. 뻗는다는 말이 좀 과격한가요. ^^

 

const 변수로 선언되면서 초기화가 된 전역(global) 변수와 const 변수로 선언되면서 초기화가 된 정적(static) 변수들이 .rodata section에 배치됩니다. 초기화가 중요한 뽀인뜨입니다. 초기화 되지 않고, 선언만 된 변수들은 .bss section에 배치됩니다. 

 

참조1의 8번 라인과 47번 라인을 보면, char * 타입 pointer로 초기화 되는 문자열 리터럴이  .rodata 영역에 배치됩니다. 포인터를 통해서 문자열이 참조 될 것이라서 문자열을 저장을 하고 있어야 참조를 할 수 있습니다. 만일 char * 타입 포인터가 아니라, 아래와 같이 char 배열 타입 변수를 초기화 하는데 문자열 리터럴이 쓰인 경우라면; 

 

    char arrayString[] = "hellow world!!";

 

이 경우 문자열 리터럴은 arrayString 배열로 복사가 되기 때문에 다시 사용할 일이 없어지는 임시 객체(object)입니다. 즉, 메모리에 저장되지 않고, 배열로 복사되고 나서는 바로 사라집니다. 이런 걸 토사구팽이라고 하나요? ^^

 

char chr = 'A';                     // 'A'는 chr에 복사하고 나면 용도 폐기

short sht = 999;                  // 999는 sht로 복사하고 나면 용도 폐기

int num = 1000;                  // 1000은 num으로 복사하고 나면 용도 폐기

 

float flt = 0.02;                    // 0.02는 .rodata 영역에 저장된다

double dble = 7.12345;      // 7.12345은 .rodata 영역에 저장된다

 

초기화에 사용되는 'A', 999, 1000과 같은 이러한 임시 객체들은 사용하고 나면 다시 사용될 일이 없기때문에 메모리에 저장할 필요가 없기때문에 즉시 용도폐기 됩니다. 반면에 float, double type의 초기화에 사용된 0.02, 7.12345는 rodata 영역에 저장됩니다.  예제 코드로 확인해 보겠습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
root@k1-duck01:~/limcs/c# cat rodata.c
#include <stdio.h>
 
int main(void)
{
    char *str    = "Hello World 1";          // "Hello World 1"은 .rodata 영역에 저장된다
    char arr[20= "Hello World 2";          // "Hello World 2"는 임시 객체로 사용 후 폐기된다
 
    char     chr = 'A';                      // 'A'는 chr에 복사하고 나면 용도 폐기
    short    sht = 999;                      // 999는 sht로 복사하고 나면 용도 폐기
    int      num = 1000;                     // 1000은 num으로 복사하고 나면 용도 폐기
 
    float    flt = 0.02;                     // 0.02는 .rodata 영역에 저장된다
    float    fln = 2;                        // 2는 .rodata 영역에 저장된다
    double  dble = 7.12345;                  // 7.12345은 .rodata 영역에 저장된다
    double  dbln = 7;                        // 7.12345은 .rodata 영역에 저장된다
 
    printf("str = %s", str);                 // "str = %s"는 .rodata 영역에 저장된다
    printf("arr = %s", arr);                 // "arr = %s"는 .rodata 영역에 저장된다
 
    printf("chr = %c, sht = %d, num = %d\n", chr, sht, num);
    printf("flt = %f, dble = %f\n", flt, dble);
 
    scanf(" %c %d",&chr, &num);
 
    return 0;
}
 
root@k1-duck01:~/limcs/c# gcc ---masm=intel rodata.c
root@k1-duck01:~/limcs/c# ls -ltr rodata.s
-rw-r--r-- 1 root root 11948 Feb 10 01:12 rodata.s
 
root@k1-duck01:~/limcs/c# cat rodata.s | more
        .file   "rodata.c"
        .intel_syntax noprefix
        .text
.Ltext0:
        .section        .rodata
.LC0:                                         // LC : Local Constant
        .string "Hello World 1"
.LC5:
        .string "str = %s\n"
.LC6:
        .string "arr = %s\n"
.LC7:
        .string "chr = %c, sht = %d, num = %d\n"
.LC8:
        .string "flt = %f, dble = %f\n"
.LC9:
        .string " %c %d"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .file 1 "rodata.c"
        .loc 1 4 1
        .cfi_startproc
        endbr64
        push    rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6-16
        mov     rbp, rsp
        .cfi_def_cfa_register 6
        sub     rsp, 80
        .loc 1 4 1
        mov     rax, QWORD PTR fs:40
        mov     QWORD PTR -8[rbp], rax
        xor     eax, eax
        .loc 1 5 11
        lea     rax, .LC0[rip]
        mov     QWORD PTR -56[rbp], rax
        .loc 1 6 10
        movabs  rax, 8022916924116329800
        movabs  rdx, 215291817074
        mov     QWORD PTR -32[rbp], rax
        mov     QWORD PTR -24[rbp], rdx
        mov     DWORD PTR -16[rbp], 0
        .loc 1 8 14
        mov     BYTE PTR -71[rbp], 65
        .loc 1 9 14
        mov     WORD PTR -70[rbp], 999
        .loc 1 10 14
        mov     DWORD PTR -68[rbp], 1000
        .loc 1 11 14
        movss   xmm0, DWORD PTR .LC1[rip]
        movss   DWORD PTR -64[rbp], xmm0
        .loc 1 12 14
        movss   xmm0, DWORD PTR .LC2[rip]
        movss   DWORD PTR -60[rbp], xmm0
        .loc 1 13 13
        movsd   xmm0, QWORD PTR .LC3[rip]
        movsd   QWORD PTR -48[rbp], xmm0
        .loc 1 14 13
        movsd   xmm0, QWORD PTR .LC4[rip]
        movsd   QWORD PTR -40[rbp], xmm0
        .loc 1 16 5
............
... 중략 ...
............
 
        .section        .rodata
        .align 4
.LC1:
        .long   1017370378
        .align 4
.LC2:
        .long   1073741824
        .align 8
.LC3:
        .long   2906833866
        .long   1075609193
        .align 8
.LC4:
        .long   0
        .long   1075576832
        
cs

참조3. rodata section에 저장되는 문자열 리터럴

 

참조3은 rodata에 저장되는 문자열 리터럴과 상수 리터럴, 그리고 rodata에 저장되지 않고, 사용 후 바로 용도 폐기되는 임시객체 문자열 리터럴 및 상수 리터럴들의 예를 보여줍니다. 라인 6,7 그리고 라인 18 ~ 24에 사용된 문자열들이 있습니다. 라인 9 ~ 16에는 초기화에 사용된 여러 상수 리터럴들이 있습니다. 라인 29와 같이 옵션 -S를 사용하여 컴파일 하면 .s 파일이 생성됩니다. rodata.s 파일을 열어서 확인해 보면, 라인 38 ~ 50과 102 ~ 116 라인에 .rodata section이 있는 것을 확인 할 수 있습니다. 임시 객체로 사용 된 "Hello World 2"는 보이지 않습니다. char 포인터 초기화에 사용된 문자열 리터럴 "Hello World 1"이 라인 39에 LC0 에 저장된 것이 보입니다. printf와 scanf의 인자로 사용된  문자열 리터럴들도 각각 LC5 ~ LC9에 저장된 것이 보입니다. LC1 ~ LC4는 밑에 따로 .rodata section에 있습니다. 왜 거기가 있는교? float와 double type 변수 초기화에 사용되는 상수 리터럴은 연산을 위해서 메모리에 저장이 되어야 하는 것 같은데요. 정확한 이유는 지금부터 좀 찾아봐야겠습니다.

 

그럼 .init section에는 뭐가 저장될까요? 그거슨 요기 밑에 두 링크의 글을 살짝 읽어봐 보시면 내용이 나옵니다.

 

http://recipes.egloos.com/5011946 

 

Linker를 마무리 짓자 - ELF와 fromelf 까지!

- Linker란,  결국 Link시에 실제 함수 정의부의 위치와 전역변수들의 위치를 library file과 object file 에서 차례대로 조사한 후에 모두 Table로 간직하고 있다가, 그 주소를 함수호출 코드 부분에 기록

recipes.egloos.com

.init section은 두 가지 정도 용도가 있는데, 하나는 OS가 있는 시스템에서 ELF가 실행될 때 실행되기 전에 initializatoin을 하는 작은 code가 들어 있습니다. 또는 Program Header라는 것이 들어가는데, executable file이니까, program header라는 걸 만들어서, 실행하는데 필요한 몇 가지 정보를 넣어 둡니다.

출처 : http://recipes.egloos.com/5011946 

 

beefchunk.com/documentation/sys-programming/binary_formats/elf/elf_from_the_programmers_perspective/node3.html

 

The .init and .fini Sections

Next: Global Constructors and Up: ELF: From The Previous: ELF Types On an ELF system, a program consists of one executable file and zero or more shared object files. To execute such a program, the system uses those files to create a process image in memory

beefchunk.com

.fini
This section holds executable instructions that contribute to the process termination code. That is, when a program exits normally, the system arranges to execute the code in this section.

.init
This section holds executable instructions that contribute to the process initialization code. That is, when a program starts to run the system arranges to execute the code in this section before the main program entry point (called main in C programs).

.fini
이 섹션에는 프로세스 종료 코드에 기여하는 실행 가능한 지침이 있습니다. 즉, 프로그램이 정상적으로 종료되면 시스템은이 섹션의 코드를 실행하도록 정렬합니다 .

.init
이 섹션에는 프로세스 초기화 코드에 기여하는 실행 가능한 지침이 있습니다. 즉, 프로그램이 실행되기 시작하면 시스템은 기본 프로그램 진입 점 (C 프로그램에서는 main이라고 함) 이전에 이 섹션의 코드를 실행하도록 정렬합니다.

The .init and .fini sections have a special purpose. If a function is placed in the .init section, the system will execute it before the main function. Also the functions placed in the .fini section will be executed by the system after the main function returns. This feature is utilized by compilers to implement global constructors and destructors in C++.

.init 및 .fini 섹션에는 특별한 용도가 있습니다. 함수가 .init 섹션에 있으면 시스템은 주 함수보다 먼저 실행합니다. 또한 .fini 섹션에있는 함수는 주 함수가 반환 된 후 시스템에서 실행됩니다. 이 기능은 C ++에서 전역 생성자와 소멸자를 구현하기 위해 컴파일러에서 사용됩니다.

When an ELF executable is executed, the system will load in all the shared object files before transferring control to the executable. With the properly constructed .init and .fini sections, constructors and destructors will be called in the right order.

 

구글 번역기로 번역했는데, 손보지 않아도 내용을 파악하는데 무리없는 것 같습니다. 점점 자동번역이 좋아지네요. 정말 영어 공부 따로 안해도 인터넷 문서 보는거 어렵지 않게 할 수 있겠습니다. 

 

.init은 main 함수가 시작되기 전에 수행되는 code 조각이고, .fini는 main 함수가 끝나고 나서 프로그램을 정리하기 위해 수행되는 code 조각이라고 하네요. 실제로 compile을 해서 asembly를 확인해 보면, 아래 참조3 에서와 같이 .init section과 .fini section을 확인을 할 수 있습니다.  

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
root@n1-duck01:~/limcs/c# cat hello.c
#include <stdio.h>
 
int main(void)
{
        printf("Hello world!!\n");
 
        return 0;
}
root@n1-duck01:~/limcs/c# gcc -g -o hello.o hello.c // -g 옵션으로 code를 object에 포함
root@n1-duck01:~/limcs/c# objdump -S -M intel hello.o // -M intel 어셈블리 형식으로 출력
 
hello.o:     file format elf64-x86-64
 
 
Disassembly of section .init:
 
0000000000001000 <_init>:
    1000:       f3 0f 1e fa             endbr64
    1004:       48 83 ec 08             sub    rsp,0x8
    1008:       48 8b 05 d9 2f 00 00    mov    rax,QWORD PTR [rip+0x2fd9]        # 3fe8 <__gmon_start__>
    100f:       48 85 c0                test   rax,rax
    1012:       74 02                   je     1016 <_init+0x16>
    1014:       ff d0                   call   rax
    1016:       48 83 c4 08             add    rsp,0x8
    101a:       c3                      ret
 
Disassembly of section .plt:
 
0000000000001020 <.plt>:
    1020:       ff 35 9a 2f 00 00       push   QWORD PTR [rip+0x2f9a]        # 3fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
    1026:       f2 ff 25 9b 2f 00 00    bnd jmp QWORD PTR [rip+0x2f9b]        # 3fc8 <_GLOBAL_OFFSET_TABLE_+0x10>
    102d:       0f 1f 00                nop    DWORD PTR [rax]
    1030:       f3 0f 1e fa             endbr64
    1034:       68 00 00 00 00          push   0x0
    1039:       f2 e9 e1 ff ff ff       bnd jmp 1020 <.plt>
    103f:       90                      nop
 
Disassembly of section .plt.got:
 
0000000000001040 <__cxa_finalize@plt>:
    1040:       f3 0f 1e fa             endbr64
    1044:       f2 ff 25 ad 2f 00 00    bnd jmp QWORD PTR [rip+0x2fad]        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
    104b:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]
 
Disassembly of section .plt.sec:
 
0000000000001050 <puts@plt>:
    1050:       f3 0f 1e fa             endbr64
    1054:       f2 ff 25 75 2f 00 00    bnd jmp QWORD PTR [rip+0x2f75]        # 3fd0 <puts@GLIBC_2.2.5>
    105b:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]
 
Disassembly of section .text:
 
0000000000001060 <_start>:
    1060:       f3 0f 1e fa             endbr64
    1064:       31 ed                   xor    ebp,ebp
    1066:       49 89 d1                mov    r9,rdx
    1069:       5e                      pop    rsi
    106a:       48 89 e2                mov    rdx,rsp
    106d:       48 83 e4 f0             and    rsp,0xfffffffffffffff0
    1071:       50                      push   rax
    1072:       54                      push   rsp
    1073:       4c 8d 05 66 01 00 00    lea    r8,[rip+0x166]        # 11e0 <__libc_csu_fini>
    107a:       48 8d 0d ef 00 00 00    lea    rcx,[rip+0xef]        # 1170 <__libc_csu_init>
    1081:       48 8d 3d c1 00 00 00    lea    rdi,[rip+0xc1]        # 1149 <main>
    1088:       ff 15 52 2f 00 00       call   QWORD PTR [rip+0x2f52]        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    108e:       f4                      hlt
    108f:       90                      nop
 
0000000000001090 <deregister_tm_clones>:
    1090:       48 8d 3d 79 2f 00 00    lea    rdi,[rip+0x2f79]        # 4010 <__TMC_END__>
    1097:       48 8d 05 72 2f 00 00    lea    rax,[rip+0x2f72]        # 4010 <__TMC_END__>
    109e:       48 39 f8                cmp    rax,rdi
    10a1:       74 15                   je     10b8 <deregister_tm_clones+0x28>
    10a3:       48 8b 05 2e 2f 00 00    mov    rax,QWORD PTR [rip+0x2f2e]        # 3fd8 <_ITM_deregisterTMCloneTable>
    10aa:       48 85 c0                test   rax,rax
    10ad:       74 09                   je     10b8 <deregister_tm_clones+0x28>
    10af:       ff e0                   jmp    rax
    10b1:       0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]
    10b8:       c3                      ret
    10b9:       0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]
 
00000000000010c0 <register_tm_clones>:
    10c0:       48 8d 3d 49 2f 00 00    lea    rdi,[rip+0x2f49]        # 4010 <__TMC_END__>
    10c7:       48 8d 35 42 2f 00 00    lea    rsi,[rip+0x2f42]        # 4010 <__TMC_END__>
    10ce:       48 29 fe                sub    rsi,rdi
    10d1:       48 89 f0                mov    rax,rsi
    10d4:       48 c1 ee 3f             shr    rsi,0x3f
    10d8:       48 c1 f8 03             sar    rax,0x3
    10dc:       48 01 c6                add    rsi,rax
    10df:       48 d1 fe                sar    rsi,1
    10e2:       74 14                   je     10f8 <register_tm_clones+0x38>
    10e4:       48 8b 05 05 2f 00 00    mov    rax,QWORD PTR [rip+0x2f05]        # 3ff0 <_ITM_registerTMCloneTable>
    10eb:       48 85 c0                test   rax,rax
    10ee:       74 08                   je     10f8 <register_tm_clones+0x38>
    10f0:       ff e0                   jmp    rax
    10f2:       66 0f 1f 44 00 00       nop    WORD PTR [rax+rax*1+0x0]
    10f8:       c3                      ret
    10f9:       0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]
 
0000000000001100 <__do_global_dtors_aux>:
    1100:       f3 0f 1e fa             endbr64
    1104:       80 3d 05 2f 00 00 00    cmp    BYTE PTR [rip+0x2f05],0x0        # 4010 <__TMC_END__>
    110b:       75 2b                   jne    1138 <__do_global_dtors_aux+0x38>
    110d:       55                      push   rbp
    110e:       48 83 3d e2 2e 00 00    cmp    QWORD PTR [rip+0x2ee2],0x0        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
    1115:       00
    1116:       48 89 e5                mov    rbp,rsp
    1119:       74 0c                   je     1127 <__do_global_dtors_aux+0x27>
    111b:       48 8b 3d e6 2e 00 00    mov    rdi,QWORD PTR [rip+0x2ee6]        # 4008 <__dso_handle>
    1122:       e8 19 ff ff ff          call   1040 <__cxa_finalize@plt>
    1127:       e8 64 ff ff ff          call   1090 <deregister_tm_clones>
    112c:       c6 05 dd 2e 00 00 01    mov    BYTE PTR [rip+0x2edd],0x1        # 4010 <__TMC_END__>
    1133:       5d                      pop    rbp
    1134:       c3                      ret
    1135:       0f 1f 00                nop    DWORD PTR [rax]
    1138:       c3                      ret
    1139:       0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]
 
0000000000001140 <frame_dummy>:
    1140:       f3 0f 1e fa             endbr64
    1144:       e9 77 ff ff ff          jmp    10c0 <register_tm_clones>
 
0000000000001149 <main>:
#include <stdio.h>
 
int main(void)
{
    1149:       f3 0f 1e fa             endbr64
    114d:       55                      push   rbp
    114e:       48 89 e5                mov    rbp,rsp
        printf("Hello world!!\n");
    1151:       48 8d 3d ac 0e 00 00    lea    rdi,[rip+0xeac]        # 2004 <_IO_stdin_used+0x4>
    1158:       e8 f3 fe ff ff          call   1050 <puts@plt>
 
        return 0;
    115d:       b8 00 00 00 00          mov    eax,0x0
}
    1162:       5d                      pop    rbp
    1163:       c3                      ret
    1164:       66 2e 0f 1f 84 00 00    nop    WORD PTR cs:[rax+rax*1+0x0]
    116b:       00 00 00
    116e:       66 90                   xchg   ax,ax
 
0000000000001170 <__libc_csu_init>:
    1170:       f3 0f 1e fa             endbr64
    1174:       41 57                   push   r15
    1176:       4c 8d 3d 3b 2c 00 00    lea    r15,[rip+0x2c3b]        # 3db8 <__frame_dummy_init_array_entry>
    117d:       41 56                   push   r14
    117f:       49 89 d6                mov    r14,rdx
    1182:       41 55                   push   r13
    1184:       49 89 f5                mov    r13,rsi
    1187:       41 54                   push   r12
    1189:       41 89 fc                mov    r12d,edi
    118c:       55                      push   rbp
    118d:       48 8d 2d 2c 2c 00 00    lea    rbp,[rip+0x2c2c]        # 3dc0 <__do_global_dtors_aux_fini_array_entry>
    1194:       53                      push   rbx
    1195:       4c 29 fd                sub    rbp,r15
    1198:       48 83 ec 08             sub    rsp,0x8
    119c:       e8 5f fe ff ff          call   1000 <_init>
    11a1:       48 c1 fd 03             sar    rbp,0x3
    11a5:       74 1f                   je     11c6 <__libc_csu_init+0x56>
    11a7:       31 db                   xor    ebx,ebx
    11a9:       0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]
    11b0:       4c 89 f2                mov    rdx,r14
    11b3:       4c 89 ee                mov    rsi,r13
    11b6:       44 89 e7                mov    edi,r12d
    11b9:       41 ff 14 df             call   QWORD PTR [r15+rbx*8]
    11bd:       48 83 c3 01             add    rbx,0x1
    11c1:       48 39 dd                cmp    rbp,rbx
    11c4:       75 ea                   jne    11b0 <__libc_csu_init+0x40>
    11c6:       48 83 c4 08             add    rsp,0x8
    11ca:       5b                      pop    rbx
    11cb:       5d                      pop    rbp
    11cc:       41 5c                   pop    r12
    11ce:       41 5d                   pop    r13
    11d0:       41 5e                   pop    r14
    11d2:       41 5f                   pop    r15
    11d4:       c3                      ret
    11d5:       66 66 2e 0f 1f 84 00    data16 nop WORD PTR cs:[rax+rax*1+0x0]
    11dc:       00 00 00 00
 
00000000000011e0 <__libc_csu_fini>:
    11e0:       f3 0f 1e fa             endbr64
    11e4:       c3                      ret
 
Disassembly of section .fini:
 
00000000000011e8 <_fini>:
    11e8:       f3 0f 1e fa             endbr64
    11ec:       48 83 ec 08             sub    rsp,0x8
    11f0:       48 83 c4 08             add    rsp,0x8
    11f4:       c3                      ret
 
cs

참조4. compile 해서 assembly로 보면 비로서 보이는 것들

 

참조4의 16 라인에 section .init 이 있고, 53 라인에 section .text 가 보입니다. 그리고 188 라인에 section .fini가 보입니다. 

 

Data

text 윗부분에 data가 배치됩니다. read wirte가 가능한 초기화 된 전역변수(global variable)와 정적변수(static variable)가 data 영역에 배치됩니다. 여기에서도  초기화가 중요한 뽀인뜨입니다. 초기화 되지 않은 전역 변수와 정적변수는 역시나 .bss 영역에 배치됩니다. 참조1의 13 라인과 44 라인과 같이 함수 내부에서도 초기화 된 정적변수(static variable)는 data 영역에 배치됩니다. 함수 내부(local)에 선언이 되어 함수 내부에서만 access 가능할 지라도 함수를 벋어나도 소멸하지 않고, 다시 함수가 재실행될 때에 다시 참조가 되어 읽고 쓰고 할 수 있어야 하기 때문에 메모리에 저장이 되어 있어야 합니다. 읽고 쓰고가 가능한 data 영역에 배치되는 이유입니다.

 

Bss (Block Started by Symbol)

초기화 되지 않은 전역변수와 정적변수가 bss 영역에 배치됩니다. 이름 그대로 Symbol 만 있고, 데이터는 내용이 없이 모두 0으로 초기화되어 있습니다.

 

 Heap

메모리를 malloc, calloc, realloc 등을 이용해서 동적 할당 하는 경우에 메모리가 heap 영역에서 할당 됩니다. 낮은 주소 번지의 메모리부터 할당이 되면서 점점 높은 번지 주소 메모리들이 할당 됩니다. 그림1에서와 같이 heap은 위로 자란다고 이야기 합니다. 

 

Stack

Stack은 번지수가 높은 곳에서부터 시작해서 낮은 곳으로 자란다고 하는데요. 예제 함수의 출력 결과에서 recursive 함수로 재귀 호출하는 stackFunction(12345678910); 의 실행 출력 결과를 보면 이해가 되실 겁니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Repeate = 1
 
&a1 local function param      :Stack    = 0x7ffe6b487b8c    // 함수 매개 변수도 stack 영역에 배치된다
&a2 local function param      :Stack    = 0x7ffe6b487b88    // 앞에서부터 push push 밑으로 자란다
&a3 local function param      :Stack    = 0x7ffe6b487b84
&a4 local function param      :Stack    = 0x7ffe6b487b80
&a5 local function param      :Stack    = 0x7ffe6b487b7c
&a6 local function param      :Stack    = 0x7ffe6b487b78    // Linux OS 에서는 매개변수 6개까지만 
                                                            // local 변수로 copy 되고
&a7 local function param      :Stack    = 0x7ffe6b487ba0    // 7번째 변수부터는 caller 함수의 stack에 저장한다
&a8 local function param      :Stack    = 0x7ffe6b487ba8    // 맨 뒤에 변수부터 push push 되어 있다
&a9 local function param      :Stack    = 0x7ffe6b487bb0
&a10 local function param     :Stack    = 0x7ffe6b487bb8
 
Repeate = 2
 
&a1 local function param      :Stack    = 0x7ffe6b487b3c    // 함수 매개 변수도 stack 영역에 배치된다
&a2 local function param      :Stack    = 0x7ffe6b487b38    // 앞에서부터 push push 밑으로 자란다
&a3 local function param      :Stack    = 0x7ffe6b487b34
&a4 local function param      :Stack    = 0x7ffe6b487b30
&a5 local function param      :Stack    = 0x7ffe6b487b2c
&a6 local function param      :Stack    = 0x7ffe6b487b28    // Linux OS 에서는 매개변수 6개까지만 
                                                            // local 변수로 copy 되고
&a7 local function param      :Stack    = 0x7ffe6b487b50    // 7번째 변수부터는 caller 함수의 stack에 저장한다
&a8 local function param      :Stack    = 0x7ffe6b487b58    // 맨 뒤에 변수부터 push push 되어 있다
&a9 local function param      :Stack    = 0x7ffe6b487b60
&a10 local function param     :Stack    = 0x7ffe6b487b68
 
Repeate = 3
 
&a1 local function param      :Stack    = 0x7ffe6b487ae     // 함수 매개 변수도 stack 영역에 배치된다
&a2 local function param      :Stack    = 0x7ffe6b487ae8    // 앞에서부터 push push 밑으로 자란다
&a3 local function param      :Stack    = 0x7ffe6b487ae4 
&a4 local function param      :Stack    = 0x7ffe6b487ae0
&a5 local function param      :Stack    = 0x7ffe6b487adc
&a6 local function param      :Stack    = 0x7ffe6b487ad8    // Linux OS 에서는 매개변수 6개까지만 
                                                            // local 변수로 copy 되고
&a7 local function param      :Stack    = 0x7ffe6b487b00    // 7번째 변수부터는 caller 함수의 stack에 저장한다
&a8 local function param      :Stack    = 0x7ffe6b487b08    // 맴 뒤에 변수부터 push push 되어 있다
&a9 local function param      :Stack    = 0x7ffe6b487b10
&a10 local function param     :Stack    = 0x7ffe6b487b18
 
cs

참조5. 높은 번지에서부터 낮은 번지로 자라는 stack 메모리

 

참조1의 라인 10에 stackFunction이 있습니다. 10개의 매개변수를 가지고 재귀 호출(recursive call)을 하는 함수로 만들어 보았습니다. 함수가 재귀 호출될 때마다 stack 메모리가 점점 아래로 아래로 내려가는 것을 보여 주고자 예제로 만들어 보았습니다.  참조5의 결과를 보면, 10개의 매개변수 중 앞에 6개와 뒤에 4개의 메모리 주소 위치가 다른 것을 알 수 있습니다. Linux 에서는 6개의 매개변수만 callee(호출된 함수)의 stack으로 copy해서 전달하고, 7번째부터는 stack에 copy하지 않고 caller(호출한 함수)의 stack에 저장해서 전달합니다. 이 것은 다른 OS에서는 다릅니다. Windows 10은 아래 참조6와 같습니다. 함수를 호출할 수록 stack 메모리가 점점 아래로 아래로 내려가는 것은 동일하지만, 매개변수 10개가 모두 stack에 copy되는 것을 볼 수 있습니다. 그리고 맨뒤에 매개변수부터 stack에 push 되는 것도 linux와 다른 점입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Repeate = 1
 
&a1 local function param      :Stack    = 000000000062FD90   // Windows 10 맨 뒤 변수부터 push push
&a2 local function param      :Stack    = 000000000062FD98
&a3 local function param      :Stack    = 000000000062FDA0
&a4 local function param      :Stack    = 000000000062FDA8
&a5 local function param      :Stack    = 000000000062FDB0
&a6 local function param      :Stack    = 000000000062FDB8
 
&a7 local function param      :Stack    = 000000000062FDC0
&a8 local function param      :Stack    = 000000000062FDC8
&a9 local function param      :Stack    = 000000000062FDD0   
&a10 local function param     :Stack    = 000000000062FDD8   // Windows 10 맨 뒤 변수부터 push push
 
Repeate = 2
 
&a1 local function param      :Stack    = 000000000062FD30   // Windows 10 맨 뒤 변수부터 push push
&a2 local function param      :Stack    = 000000000062FD38
&a3 local function param      :Stack    = 000000000062FD40
&a4 local function param      :Stack    = 000000000062FD48
&a5 local function param      :Stack    = 000000000062FD50
&a6 local function param      :Stack    = 000000000062FD58
 
&a7 local function param      :Stack    = 000000000062FD60
&a8 local function param      :Stack    = 000000000062FD68
&a9 local function param      :Stack    = 000000000062FD70
&a10 local function param     :Stack    = 000000000062FD78   // Windows 10 맨 뒤 변수부터 push push
 
Repeate = 3
 
&a1 local function param      :Stack    = 000000000062FCD0   // Windows 10 맨 뒤 변수부터 push push
&a2 local function param      :Stack    = 000000000062FCD8
&a3 local function param      :Stack    = 000000000062FCE0
&a4 local function param      :Stack    = 000000000062FCE8
&a5 local function param      :Stack    = 000000000062FCF0
&a6 local function param      :Stack    = 000000000062FCF8
 
&a7 local function param      :Stack    = 000000000062FD00
&a8 local function param      :Stack    = 000000000062FD08
&a9 local function param      :Stack    = 000000000062FD10
&a10 local function param     :Stack    = 000000000062FD18   // Windows 10 맨 뒤 변수부터 push push
cs

참조6. Windows 10에서 Dev-C++에서 컴파일하고 실행 했어요

 

다른 OS들에서는 돌려 보지를 않아서 어떨지 정확히는 모르겠습니다. 단, Solaris, Linux, FreeBSD, macOS에서 사용하는 함수 calling convetion이 "System V AMD64 ABI"로 같으니 Linux와 같을 것 같습니다.

"포인터는 메모리 주소다"

"Pointer is an address of memory"

 

포인터를 공부하기에 앞서 포인터를 잘 이해하는 데 도움이 되는 기초체력 다지기로 C 프로그래밍 메모리 레이아웃에 대해서 알아 보았습니다.  다음 글에서 기초체력을 빠방하게 키워주는 몇가지 더 공부해 볼꺼에요. 세번째 글에서부터 실제로 "살아있는 C 언어 실세", 포인터에 대해서 본격적으로 쉽게 쏙쏙 이해가 되는 방법으로 설명을 시작하게 될 것 같습니다. 기대해 주세요.

 

C 프로그래밍에 도전했다가 포인터 때문에 포기하는 포포자가 없는 세상을 위하여 오리뎅이이가 함께 만들어 가겠습니다.  (이기 무슨 정당이나? ^^)

 

뼈때리는 지적의 댓글도, 힘내라 북돋아 주는 격려의 댓글도, 좀 더 디테일을 원하거나 관련된 지식에 대해 질문을 하거나 하는 댓글도 너무 너무 환영합니데이~~~ 댓글 달아 주이~~~~소!!

 

2021년 1월 9일 수원에서 뒤뚱뒤뚱~~~~ [오리]