[오리뎅이의 C 포인터 이야기 - 1] 포인터를 쉽게 배울수 있을까요?
오리뎅이의 C 포인터 이야기안녕하세요? 오리뎅이입니다.
C를 포기하는 가장 큰 이유가 포인터 때문이라는 얘기를 많이 들었습니다. 포인터 난관을 뚫고 C 프로그래밍을 잘 하시는 분들은 포인터를 너무 좋아하고 잘 다루시는데요. 알고 나면, 쉬운게 왜 처음에 배우는 게 그리 어려운 것일까? 쉽게 배울 수는 없을까? 그 해답을 찾기 위해서 제 스스로 카페에 올라오는 포인터에 대한 수많은 종류의 질문들을 보면서 무엇을 어려워 하는지 연구하고, 포인터에 대한 칼럼이나 포인터를 다룬 교재들에서 어떻게 가르치고 있는지를 보면서 왜 배우기가 어려운 건지를 생각해 보면서 쉽게 터득할 수 있도록 가르치는 방법을 고민했습니다.
이거슨 순전히 포포자(포인터 때문에 C 포기한 자 ㅠ.ㅠ)분들과 이제 C를 시작하시고 포인터를 막 시작하신 분들을 독자로 생각하고 쓰는 이야기입니다. 이미 포인터 산맥을 넘으신 분들이 혹시라도 이 글을 보시게 된다면, 틀린 얘기 하고 있는 거 없는지? 정말로 이렇게 가르치면 초보자나 포포자 분들이 포인터 난관을 쉽게 헤쳐 나갈 수 있을 거 같은지? 살펴 봐 주시고, 잘 못된 부분에 대한 지적이나 더 나은 방법에 대한 조언을 해 주시면 좋겠습니다.
C 프로그래밍 memory layout
포인터를 공부하기에 앞서서 기초 체력을 키우기 위해 꼭 필요한 사전 지식 몇가지를 먼저 알아 보려고 합니다. 첫번째로 "메모리 레이아웃"입니다.
"포인터는 메모리 주소다"
"Pointer is an address of memory"
뒤에서 좀 더 자세히 다루겠지만, 포인터는 메모리 주소입니다. 메모리를 이해 하는 것이 포인터를 쉽게 이해하기 위한 첫번째 기초 체력입니다. C 언어의 메모리 레이아웃에 대해서 정리한 글을 몇개 찾아서 아래에 링크를 걸어 놓았습니다. 각 링크 한번씩 방문하셔서 쓰윽 훑어 봐 주세요. 광고 아닙니다. ^^. 어려운 내용들이기때문에 내용을 다 이해하시면서 읽으실 필요는 없습니다. 포인터 공부를 하기 위한 기초체력을 쌓기 위한 것이라서 변수들이 메모리에 이렇게 배치가 되는 구나!! 정도만 살짝 알아 주시면 됩니다.
https://gusdnd852.tistory.com/16
blog.naver.com/justkukaro/220681279377
https://bigpel66.tistory.com/9
그림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(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
}
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(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
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 -g -S -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
.init section은 두 가지 정도 용도가 있는데, 하나는 OS가 있는 시스템에서 ELF가 실행될 때 실행되기 전에 initializatoin을 하는 작은 code가 들어 있습니다. 또는 Program Header라는 것이 들어가는데, executable file이니까, program header라는 걸 만들어서, 실행하는데 필요한 몇 가지 정보를 넣어 둡니다.
출처 : http://recipes.egloos.com/5011946
.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(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 의 실행 출력 결과를 보면 이해가 되실 겁니다.
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일 수원에서 뒤뚱뒤뚱~~~~ [오리]
'오리뎅이의 C 포인터 이야기' 카테고리의 다른 글
[오리뎅이의 C 포인터 이야기 - 5] 배열의 포인터 변환 (3) | 2021.01.30 |
---|---|
[오리뎅이의 C 포인터 이야기 - 4] 포인터의 영원한 동반자 배열 (1) | 2021.01.24 |
[오리뎅이의 C 포인터 이야기 - 3] 오리뎅이 포인터 학습법 (2) | 2021.01.17 |
[오리뎅이의 C 포인터 이야기 - 2] 포인터는 주소다 (1) | 2021.01.12 |