오리뎅이의 네떡세상

[오리뎅이의 C 포인터 이야기 - 5] 배열의 포인터 변환

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

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

 

 

사진은 제가 인도 뭄바이에 주재원으로 있을 때, 패밀리 회원권을 끊어서 주말마다 가던 BPGC(Bombay Presidency Golf Club)의 클럽 하우스 바로 앞에 있는 야외 풀장 모습입니다. 사진을 보고 있자니, 저 골프장에 주말마다 갔었으면서도 정작 저 풀장은 한번도 들어가 본 적은 없었네요. 뜨거운 열사의 나라 사우디 리야드 골프 코스(RGC)에서 골프를 배우고, 만만치 않게 덮고 습한 인도 뭄바이에서 골프를 즐겼었지만, 정작 한국에 돌아와서는 골프채를 1년에 한번 잡을까 말까하네요. 골프장이 몇 개 없어서 부킹도 어렵던 나라에서는 열심히 골프를 즐겼는데, 동네 방네 발길에 채이는 게 골프장인 한국에서는 골프보다 재미난 놀거리가 많아서인지 안땡겨요. ^^

 

 

포인터와 함께 C 학습을 어렵게 하는 또 하나의 암초같은 존재가 배열입니다. 배열이 처음 배울 때 어려운 이유는 바로 오늘 학습하게 될 배열이 포인터로 변환되는 특성때문입니다. 그런데 어렵게 느껴지게 만드는 배열의 포인터 변환 특성만 정확히 알고 있으면 배열도 포인터와 마찬가지로 딱 보면 척 알 수 있습니다. ^^

배열과 포인터

[오리뎅이의 C 포인터 이야기 - 2] 편에서 C 언어의 object(객체)를 설명하기 위해서 인용했었던 "C11 ISO/IEC 9899:201x 표준 문서6.3.2.1"에는 다음과 같이 array와 관련된 부분이 있습니다.

3 Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.

sizeof 연산자 또는 단항 & 연산자의 피연산자 이거나 배열을 초기화하는 데 사용되는 문자열 리터럴 인 경우를 제외하고, "array of type'' type을 가진 표현식 배열 객체(object)의 첫 번째 요소를 가리키는 "pointer to type"  type을 가진 표현식으로 변환(conversion)되고, lvalue가 아니다. 배열 객체가 레지스터 스토리지 클래스를 가지면, 정의되지 않은 동작입니다.

출처: C11 ISO/IEC 9899:201x 표준 문서 6.3.2.1절 3항

참조1. C11 표준 6.3.2.1절 3항 내용

 

딱 3가지 경우를 제외하고, '"array of type" type을 가진 표현식은 배열 객체(object)의 첫 번째 요소를 가리키는 "pointer to type"의 type을 가진 표현식으로 변환(conversion)되며,  lvalue가 아니다' 라고 설명하고 있습니다. 다음의 배열이 포인터로 변환되지 않는, 즉 제외되는 딱 3가지 경우의 예입니다.

 

int iArr[2];                                   // array [3] of int

1.  sizeof 의 피연산자
    printf("%d", sizeof (iArr));                                                           

2. 단항 &(ampersand) 연산자의 피연산자

    printf("%p", &iArr);                                                                       

3. 배열을 초기화하는 데 사용되는 문자열 리터럴
   char cArr[12] = "Hello world";      // 문자열 리터럴 "Hello world"은 'array [12] of char' 타입이고,
                                               // char 배열 변수를 초기화 하는데 사용될 때는 배열 그 자체로 사용    

참조2. 배열이 pointer로 변환 되지 않는 딱 3가지 경우

 

참조2에 기술한 딱 3가지 경우에는 배열은 pointer로 conversion이 일어나지 않고, 배열 그 자체로 사용되는 경우입니다. 배열이 참조2의 딱 3가지 경우로 사용되지 않을 때에는 배열의 첫 번째 요소를 가리키는 pointer로 conversion 됩니다. 

 

배열이 pointer로 변환 되는 경우에도 법칙이 있다

배열과 포인터는 배우 닮았습니다. 배열 인덱스 연산자, [ ]는 식별자의 우측에  포인터 참조 연산자 *는 식별자의 좌측에 위치해서 마치 식별자를 기준으로 거울을 마주 보고 있는듯한 느낌입니다. 포인터와 관련된 배열의 특징들을 나열해 보면 다음과 같습니다.

-, 배열은 그 배열의 첫번째 요소를 가리키는 포인터로 변환 된다.
   N차원의 다차원 배열은 그 배열의 첫 번째 요소인 N-1차원 배열 포인터로 변환된다.
-, 배열의 연산자 [] 하나는 포인터 연산자 * 하나와 서로 매칭된다.
   int *iPtr;      // iPtr은 첫번째 객체로 포인터 type, *iPtr 은 두 번째 객체 int type
   int aArr[2];   // aArr은 배열 객체 그자체 int [2] 배열, *aArr은 배열의 첫 번째 요소 객체 aArr[0] 

-, 배열은 lvalue가 아니다.
   1차원 배열의 요소만이 lvalue이다.
   배열 p가 2차원 배열이면, *p는 1차원 배열이고, 배열이므로 lvalue가 아니다.
   배열 p가 2차원 배열이면, **p는 1차원 배열의 요소이고, lvalue이다.
   배열 p가 3차원 배열이면, *p는 2차원 배열이고, 배열이므로 lvalue가 아니다.
   배열 p가 3차원 배열이면, **p는 1차원 배열이고, 배열이므로 lvalue가 아니다.
   배열 p가 3차원 배열이면, ***p는 1차원 배열의 요소이고, lvalue이다.
   배열은 lvalue가 아니므로 대입 연산의 좌항 피연산자로 사용될 수 없다.
   배열은 lvalue가 아니므로 lvalue conversion 되어 value로 사용될 수 없다.

참조3. 배열의 포인터 변환 관련 특징들

 

배열의 포인터 관련 특징 중 핵심은 붉은색 굵은 글씨체로 마킹한 2가지입니다. 참조2의 딱 3가지 경우를 제외한 경우에 배열이 그 배열의 첫 번째 요소를 가리키는 포인터로 변환되는 것이 이번 이야기에서 계속해서 강조하고 있는 배열의 첫 번째 특징입니다. 두 번째 특징은 배열이 lvalue가 아니라는 것입니다. lvalue가 아니기 때문에 배열은 대입 연산자의 좌항 피연산자가 될 수 없습니다. 또한, 어느 위치에 사용되건 lvaue conversion이 되어 value로 사용될 수도 없습니다.

 

3차원 배열은  첫 번째 요소의 포인터로 변환 된다.

3차원 배열을 사용하여 배열의 포인터 변환 관련 특징들을 도식화하여 좀 더 알기 쉽게 알아 보겠습니다.

 

그림1. 3차원 배열의 포인터 변환 특징

 

1. iaaaArr[2][2][2] 은 3차원 배열입니다.

    . 영어로 타입을 표현해 보면 다음과 같습니다

      - array of [2] array of [2] array of [2] int        // array가 3번 나오고, 마지막에 한번만 int 가 나옵니다.

 

2. iaaaArr 이 &sizeof의 피연산자로 사용되면, pointer conversion 되지 않습니다.

    . &iaaaArr                                  //  그림1에서 가장 바깥쪽 네모 박스 을 가리키는 포인터(주소)입니다.

                                                      //  &iaaaArr + 1 의 더하기 연산을 하면, 의 크기 32만큼 주소가 증가

    . sizeof(iaaaArr)                        //  그림1에서 가장 바깥쪽 네모 박스 의 크기 32를 나타냅니다

 

3. iaaaArr이 &sizeof 피연산자가 아닌 용도로 사용되면, 첫 번째 요소의 포인터로 변환됩니다.

    . int (*piaaArr)[2][2] = iaaaArr;   // iaaaArr은 그림1에서 파란색 네모 박스 가리키는 포인터입니다.

                                                          // 첫 번째 요소를 가리키는 &iaaaArr[0] 로 변환됩니다. 

                                                          //  포인터로 변환 되지만 expression의 결과 값인 rvalue입니다.

 

4. *iaaaArr은 2차원 배열입니다.

    . &*iaaaArr                                   //  그림1에서 파란색 네모 박스 를 가리키는 포인터(주소)입니다.

                                                          //  &*iaaaArr + 1 의 더하기 연산을 하면, 의 크기 16만큼 주소가 

                                                          //  증가하여, 의 포인터가 됩니다.

    . sizeof (*iaaaArr)                        //  그림1에서 파란색 네모 박스 의 크기 16을 나타냅니다.

    . int (*piaArr)[2] = *iaaaArr;       //  참조 연산자 *를 붙이면, 두 번째 배열 객체 가  됩니다.

                                                          //  iaaaArr[0] 도 2차원 배열이므로, &와 sizeof의 피연산자가 아니면 다시

                                                          //  첫 번째 요소를 가리키는 &iaaaArr[0][0]로 변환됩니다.

                                                          //  포인터로 변환 되지만 expression의 결과 값인 rvalue입니다.

    

5. **iaaaArr은 1차원 배열입니다.

    . &**iaaaArr                                 //  그림1에서 빨간색 네모 박스를 가리키는 포인터(주소)입니다.

                                                          //  &**iaaArr + 1 의 더하기 연산을 하면, 의 크기 8만큼 주소가

                                                          //  증가하여, 의 포인터가 됩니다.

    . sizeof (**iaaaArr)                      //  그림1에서 빨간색 네모 박스  의 크기 8을 나타냅니다.

    . int *piArr = **iaaaArr;              //  참조 연산자 *를 2개 붙이면, 세 번째 배열 객체 이  됩니다.

                                                          //  iaaaArr[0][0]도 1차원 배열이므로, &와 sizeof의 피연산자가 아니면

                                                          //  첫 번째 요소를 가리키는 &iaaaArr[0][0][0]로 변환됩니다.

                                                          //  포인터로 변환 되지만 expression의 결과 값인 rvalue입니다.

 

6. ***iaaaArr은 1차원 배열의 첫 번째 요소입니다.

    . &***iaaaArr                               //  그림1에서 검정색 네모 박스를 가리키는 포인터(주소)입니다.

                                                          //  &***iaaArr + 1 의 더하기 연산을 하면, 크기 4만큼 주소가 증가

    . sizeof (***iaaaArr)                    //  그림1에서 검정색 네모 박스 크기 4를 나타냅니다.

    . int iArr = ***iaaaArr;                //  참조 연산자 *를 3개 붙이면, 네 번째 int 객체 ④, 즉, 0이 됩니다.

                                                         //  3차원 배열에 *을 3개 붙이면, 1차원 배열 요소이고, lvalue입니다.

                                                         

3차원 배열은 3차원 포인터마냥 참조 연산자 *를 3개 붙이니 최종 1차원 배열의 요소 객체, lvalue가 되었습니다. 

 

그림1에서 , ②, , 4가지 주소는 크기는 다 다르지만, 시작 위치가 같으므로 모두 같은 주소입니다.

①=&iaaaArr,②=iaaaArr(=&iaaaArr[0]),=iaaaArr[0](=&iaaaArr[0][0]),④=iaaaArr[0][0](=&iaaaArr[0][0][0])

 

처음 배울 때, ,②,,4가지 주소를 출력했는데, 모두 동일한 주소가 나오면, 눈이 땡그래집니다. "오잉!! 왜 다 같게 나오지?" ㅎㅎㅎ. 이제는 눈 땡그래지지 않습니다.

 

1차원 배열은 첫 번째 요소를 가리키는 단일 포인터로 변환된다.

3차원 배열로 알아 본 내용을 1차원 배열과 2차원 배열의 설명과 코드 예제로 복습해 보겠습니다. 

 

그림2. 1차원 배열의 포인터 변환 특징

 

sizeof 연산자의 피연산자 그리고 단항 연산자 &의 피연산자로 배열이 사용될 때, 배열은 배열 그 자체로 사용됩니다. 그림2의 1차원 배열에서 sizeof (iaArr)의 결과는 붉은색 curly bracket, { } 이 나타내는 배열 전체의 크기입니다. &iaArr의 결과도 { } 이 나타내는 배열 전체 크기의 객체(object) 대한 pointer 입니다. iaArr이 sizeof의 피연산자 또는 단항 연산자 &의 피연산자가 아닌 경우에는 iaArr은 그림1에서 배열의 첫 번째 요소, iaArr[0], 즉, 정수 1을 담고 있는 객체를 가르키는 int 형 단일 포인터로 변환됩니다. 배열이 포인터로 변환되더라도 포인터를 저장할 수 있는 메모리 공간을 가진 변수가 아닌 단지 주소를 나타내는 rvalue입니다. 즉, 배열이 포인터로 변환되는 경우 포인터 변수가 아닌 포인터 상수로 변환됩니다. 따라서, 대입연산자의 좌항 피연산자로 사용될 수 없고, iaArr++ 이나 --iaArr과 같은 포인터 자체를 증감하는 연산도 불가능합니다. 

 

1차원 배열의 예제 코드입니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
 
int main(void)
{
    int iArr[2= {01};   // iArr은 array of [2] int
    int *piArr;             // piArr은 pointer to int
 
    piArr = iArr;           // iArr = &iArr[0], 첫 번째 요소를 가리키는 단일 포인터로 변환
                            // 1차원 배열 요소는 lvaue이므로 *iArr은 lvalue
    *iArr  = 10;            // 대입 연산자의 좌항 피연산자로 사용가능, *iArr = iArr[0] = 10
    *piArr = *iArr + 1;     // lvalue conversion, *iArr = iArr[0] = 10 
                            // piArr = iArr 이므로 iArr[0] = 11
    *(iArr + 1= 20;       // 포인터의 더하기 연산, *(iArr + 1) = iArr[1] = 20
                            // 대입 연산의 좌항 피연산자는 1차원 배열만 가능
 
    printf("iArr[0] = %d, iArr[1] = %d\n", iArr[0], iArr[1]);
 
   return 0;
}
 
-------------------------------------------------------------------------------------------------
 
iArr[0= 11, iArr[1= 20
cs

참조4. 1차원 배열의 포인터 변환 특징

 

2차원 배열은  첫 번째 1차원 배열 요소 포인터로 변환 된다.

그림3. 2차원 배열의 포인터 변환 특징

 

그림3의 2차원 배열에서 sizeof (iaaArr)의 결과는 파란색 curly bracket, { } 이 나타내는 배열 전체의 크기입니다. &iaaArr의 결과도 { } 이 나타내는 배열 전체 크기의 객체(object) 대한 pointer 입니다. 그렇기 때문에 &iaaArr + 1의 더하기 연산 결과는 배열 전체 크기 16 bytes 만큼 증가한 주소가 됩니다. iaaArr이 sizeof의 피연산자 또는 단항 연산자 &의 피연산자가 아닌 경우에는 iaaArr은 그림2에서 은색 curly bracket, { 1, 2 } 을 가리키는 첫 번째 요소인 1차원 배열의 포인터로 변환됩니다. 포인터 참조 연산자가 붙으면, *iaaArr은 1차원 배열 {1, 2}, 그 자체입니다. sizeof나 & 연산자로 사용되면, 역시나 1차원 배열 그 자체를 나타내지만, 그 이외의 경우에는 다시 포인터 변환이 일어나서 1차원 배열의 첫 번째 요소를 가리키는 포인터로 변환되고, **iaaArr은 iaaArr[0][0]으로 숫자 1이 저장된 int 객체를 나타내는 lvalue 입니다.

 

2차원 배열의 예제 코드입니다. 

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
#include <stdio.h>
 
int main(void)
{
    int iaaArr[2][3= {01234 }; // array of [2] array of [3] int
    int (*paiArr)[3];                    // pointer to array of [3] int
    int *piArr0, *piArr1;
 
    paiArr = iaaArr;            // 이차원 배열은 1차원 배열 포인터로 변환
                                    
    printf("&iaaArr   = %p, sizeof(iaaArr)   = %d, iaaArr  = %p\n"
            &iaaArr,      sizeof(iaaArr),          iaaArr);                                
    printf("&*iaaArr  = %p, sizeof(*iaaArr)  = %d, *iaaArr = %p\n"
            &*iaaArr,      sizeof(*iaaArr),        *iaaArr);                                  
    printf("&**iaaArr = %p, sizeof(**iaaArr) = %d, **iaaArr = %d\n"
            &**iaaArr,      sizeof(**iaaArr),      **iaaArr);                              
                                         
    printf("\n");
    piArr0 = iaaArr[0];         // 1차원 배열은 단일 포인터로 변환
 
    printf("&iaaArr[0] = %p, sizeof(iaaArr[0]) = %d, iaaArr[0] = %p\n"
            &iaaArr[0],      sizeof(iaaArr[0]),      iaaArr[0]);
    printf("**iaaArr     = %d, *iaaArr[0]     = %d, iaaArr[0][0] = %d\n"
            **iaaArr,          *iaaArr[0],          iaaArr[0][0]);  //  0 
    printf("*(*iaaArr+1) = %d, *(iaaArr[0]+1) = %d, iaaArr[0][1] = %d\n"
            *(*iaaArr+1), *(iaaArr[0]+1),           iaaArr[0][1]); //  1 
 
    printf("\n");                                     
    piArr1 = iaaArr[1];         // 1차원 배열은 단일 포인터로 변환
 
    printf("&iaaArr[1] = %p, sizeof(iaaArr[1]) = %d, iaaArr[1] = %p\n"
            &iaaArr[1],      sizeof(iaaArr[1]),      iaaArr[1]); 
    printf("**(iaaArr+1)     = %d, *iaaArr[1]     = %d, iaaArr[1][0] = %d\n"
            **(iaaArr+1), *iaaArr[1],                   iaaArr[1][0]);  //  3 
    printf("*(*(iaaArr+1)+1) = %d, *(iaaArr[1]+1) = %d, iaaArr[1][1] = %d\n"
            *(*(iaaArr+1)+1), *(iaaArr[1]+1),           iaaArr[1][1]);  //  4 
 
   return 0;
}
 
-------------------------------------------------------------------------------------------------
 
&iaaArr   = 000000000062FDF0, sizeof(iaaArr)   = 24, iaaArr  = 000000000062FDF0
&*iaaArr  = 000000000062FDF0, sizeof(*iaaArr)  = 12*iaaArr = 000000000062FDF0
&**iaaArr = 000000000062FDF0, sizeof(**iaaArr) = 4**iaaArr = 0
 
&iaaArr[0= 000000000062FDF0, sizeof(iaaArr[0]) = 12, iaaArr[0= 000000000062FDF0
**iaaArr     = 0*iaaArr[0]     = 0, iaaArr[0][0= 0
*(*iaaArr+1= 1*(iaaArr[0]+1= 1, iaaArr[0][1= 1
 
&iaaArr[1= 000000000062FDFC, sizeof(iaaArr[1]) = 12, iaaArr[1= 000000000062FDFC
**(iaaArr+1)     = 3*iaaArr[1]     = 3, iaaArr[1][0= 3
*(*(iaaArr+1)+1= 4*(iaaArr[1]+1= 4, iaaArr[1][1= 4
cs

참조5. 2차원 배열의 포인터 변환 특징

 

참조5의 코드를 카피해서 본인의 로컬 머신에서 직접 컴파일하고, 실행하셔서 코드 행간 내용과 출력 결과를 비교해 보세요.

 

2차원 배열 iaaArr[2][3]는 int 형 요소 3개를 가진 1차원 배열 2개를 가진 2차원 배열입니다. 크기는 2 x 3 x 4 = 24입니다. 1차원 배열은 int 형 요소 3개이므로 크기가 3 x 4 = 12입니다. int 자료형의 크기는 제 PC에서는 4이군요. 11 ~ 16 라인에서 주소와 크기를 축력해 본 결과가 43 ~ 45 라인에 있습니다. 주소는 모두 동일하게 나옵니다. 크기는 각각 다릅니다.

 

33 ~ 34 라인에서 iaaArr+1 의 포인터 더하기 연산을 하였습니다. 그랬더니, 1차원 배열의 크기 12만큼 주소가 증가해서 두 번째 1차원 배열을 가리키는 포인터가 되었습니다. 2차원 배열에 * 을 2개 붙이니 1차원 배열 첫 번째 요소가 되고, lvalue conversion이 되어 3이 출력 되었습니다.

 

 

인자 배열의 포인터 변환 껌이쥬? ^^. 아래 5중 포인터와 5차원 배열의 극한 비교를 쓱 보시면 배열의 포인터 변호나 쌉 정리 가능!!  

 

그림4. 5중 포인터와 5차원 배열 영어 이름 비교

 

-, 배열은 그 배열의 첫번째 요소를 가리키는 포인터로 변환 된다.
-, 배열은 lvalue가 아니다. 
   1차원 배열의 요소만이 lvalue이다.

 

C에서 배열은 함수의 매개 변수가 될 수 없다 

배열이 C를 공부하는 나를 혼란스럽게 하는 또 한가지 경우가 배열처럼 생긴 모양으로 함수의 매개 변수로 사용되는 포인터 경우입니다. 분명한 것은 C에서는 배열을 함수 인자로 전달할 수 없습니다. 배열 처럼 생긴 함수의 매개 변수는 배열의 탈을 쓴 포인터입니다.

 

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
#include <stdio.h>
 
int SummaryArray(int piArr[], int iCnt)                      // piArr은 배열의 탈을 쓴 포인터
{                                                            // int piArr[] = int *piArr 
    int i, iSum = 0;
    
    for (i = 0; i < iCnt; i++)
    {
        iSum += piArr[i];
    }
    
    return iSum;
}
 
int main()
{
    int iArr[10= { 12345678910 };
    int iSum = 0;
 
    iSum = SummaryArray(iArr, sizeof(iArr) / sizeof(int));    // iArr = &iArr[0], 포인터 변환  
                                                               
    printf("Sum = %d\n", iSum);
    
    return 0;
}
 
-------------------------------------------------------------------------------------------------
 
Sum = 55
cs

참조6. 함수의 매개 변수로 사용 된 배열의 탈을 쓴 포인터

 

참조6의 예에서 라의 3의 SummaryArray 함수의 매개 변수로 사용된 int piArr[]은 생김새로만 봐서는 영락 없는 배열의 모습니다. 그러나 바뜨, 이거슨 배일이 아닙니다. 포인터입니다. C에서 배열 자체를 함수의 인자로 전달하는 것을 지원하지 않습니다. C가 함수의 인자 evaluation strategy가 모조리 값을 copy해서 넘기는 call by value만 지원하는 관계로 배열 변수도 값을 copy해서 넘긴다면, 엄청난 크기의 배열을 요소 하나 하나 copy해서 넘기는 건 좀 성능상 아닐 것 같아서 인 것도 같고요. 배열은 lvaue가 아니라서 배열 자체가 대입 연산자의 좌항 피연산자로 사용될 수도 없으니, 값을 copy 한다는 것도 대입의 일종으로 본다면 안되는 게 맞는거 같기도 합니다. 아뭏튼 어째튼 저째튼 C에서는 배열을 함수의 매개 변수로 사용할 수 없습니다. 대신에 뭔 수작인지 몰라도 저런 배열의 탈을 쓴 포인터 문법을 허용합니다. 생김새는 배열처럼 생겨 먹었지만, 포인터라는 것을 기억하기만 하면 한나도 안 어렵습니다.

 

[오리뎅이의 C 포인터 이야기 - 4] 에서 포인터 배열과 배열 포인터의 차이를 알아 보았었고, 배열 포인터 만드는 방법도 확인해 보았었습니다.  기억을 되새김질해 보기 위해서 참조 자료를 소환합니다. 

 

char const *apArr[10];       // 이 것은 포인터 배열인가? 배열 포인터인가? : [정답] 포인터 배열입니다.
                                   // 식별자 apArr과 [] 사이에 괄호가 없으면 배열

char const (*paPtr)[10];    // 이 것은 포인터 배열인가? 배열 포인터인가? : [정답] 배열 포인터입니다. 
                                  // 식별자 paPtr 앞에 *가 있고, 이 것들을 ()가 감싸고 있으면 포인터

 

함수의 매개 변수로 사용하는 다차원 배열의 모습을 한 인자도 역시나 배열의 탈을 쓴 포인터입니다. "이 머선 말이고?" ^^ 아래의 예제 코드를 통해서 머선 말인지 한번 확인해 보시죠.

 

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
#include <stdio.h>
 
void ArraySwap(int piaArr[][5], int iaCnt)                   // piaArr은 배열의 탈을 쓴 포인터
{                                                            // int piaArr[][5] = int (*piaArr)[5] 
    int iTemp[5], i;
    
    for (i = 0; i < 5; i++)
    {
        iTemp[i]= piaArr[0][i];
        piaArr[0][i] = piaArr[1][i];
        piaArr[1][i] = iTemp[i];
    }
    
    printf("sizeof (piaArr) = %2d, sizeof (*piaArr) = %2d\n",
            sizeof (piaArr),       sizeof (*piaArr));
}
 
int main()
{
    int iaArr[2][5= { {12345}, {678910} };
    int i, j;
 
    printf(":: Before ArraySwap()\n");
    for (i = 0; i < 2; i++)
    {
        for (j = 0; j < 5; j++)
        {
            printf("%2d ", iaArr[i][j]);
        }
        printf("\n");
    }
 
    ArraySwap(iaArr, sizeof(iaArr) / ((sizeof(int* 5)));    // iArr = &iaArr[0], 포인터 변환  
                           
    printf("\n:: After ArraySwap()\n");                              
    for (i = 0; i < 2; i++)
    {
        for (j = 0; j < 5; j++)
        {
            printf("%2d ", iaArr[i][j]);
        }
        printf("\n");
    }                                                              
        
    return 0;
}
 
-------------------------------------------------------------------------------------------------
 
:: Before ArraySwap()
 1  2  3  4  5
 6  7  8  9 10
 
sizeof (piaArr) =  8sizeof (*piaArr) = 20
 
:: After ArraySwap()
 6  7  8  9 10
 1  2  3  4  5
cs

참조7. 2차원 배열의 탈을 쓴 배열 포인터

 

참조 7의 예제 코드에서 ArraySwap() 함수의 매개 변수인 int piaaArr[][5] 은 생긴 것은 딱 2차원 배열의 모습을 하고 있습니다. 배열인 것 같지만, 겉모양에 속으면 안됩니다. ^^.  C는 배열을 함수의 인자로 넘기지 못합니다. 함수 매개 변수에 배열처럼 생긴 것이 있으면, 1차원 배열 모양이건 3차원 배열 모양이건 무조건 포인터입니다.

 

 

배열이 은제는 포인터로 변환되고 은제는 아니되는지 딱 보믄 알것지라~이? ^^. C 포인터 이야기 끝이 보이네요. 다음 이야기에서는 또 하나의 복병 함수 포인터에 대해서 신박한 예제들을 가지고 썰을 풀어 볼 예정입니다. 함수 포인터만 이야기 하면, 공부할 주제들은 다 끝난 것이어요. 오늘도 끝까지 [오리뎅이 C 포인터 이야기]를 읽어 주셔서 고맙습니다.

 

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

 

2021년 1월 30일 수원에서 [오리뎅이]

[오리뎅이의 C 포인터 이야기 - 4] 포인터의 영원한 동반자 배열

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

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

 

 

사진은 제가 사우디 리야드에서 주재원으로 근무하던 마지막 해(2013년)에 가족들은 한국으로 돌아 가고, 혼자서 게스트 하우스에서 생활하던 때에 이른 아침 리야드 시내를 산책하다가 동 터오는 동쪽 하늘을 찍은 사진이에요. 딱  보기에는 마치 해지는 황혼녘은 서쪽 하늘을 생각나게 하는 그림인데, 해 떠오르는 아침 동쪽 하늘 사진이란게 신기하지 않나요?(저만 그런가요? ㅎ) 그 척박한 나라에서의 4년, 그 때는 너무 힘들었었지만 지나고 나니 아릿한 축억으로 남아 다시 돌아가 보고 싶다는 생각도 가끔 드네요.

 

 

오늘 [오리뎅이 C 포인터 이야기]에서는 어찌나 끈끈한지 뗄래야 뗄 수 없는 포인터와 배열의 그 끈끈한 유대관계를 낱낱히 해부해 보겠습니다.  배열과 포인터의 관계를 자세히 들여다 보기 전에 우선 배열이 익숙치 않으신 분들을 위해서 배열의 밥 아조씨, BoB(Basic of Basic) 먼저 살펴 보겠습니다. 

 

배열이 뭐열?

배열이란 char, short, int, double, struct, array 등의 자료형(data type)들에 대해서 같은 type의 자료형유한한 크기로 나열하는 자료형입니다. 배열을 구성하는 같은 type의 자료들을 요소(element)라고 합니다. 다음은 int type 요소를 5개 가진 1차원 배열 변수를 선언한 예입니다.

 

   int iArr[5];            // 1차원 배열 선언 문법  :  type name[size]; 
                           //                                  size =  배열 요소의 수 or 배열 크기

 

배열의 type으로는 void type을 제외 한 기본 자료형(그림1 참조)과 포인터, 열거(enum) 형 또는 크기가 정해진 다른 배열(이 경우 배열을 다차원 배열이라고 함)이 사용될 수 있습니다. 

 

그림1. C언어 기본 자료형

그림1 출처 : m.blog.naver.com/sharonichoya/220339079484

 

크기가 정해지지 않은 배열은 int iArr[]; 와 같이 배열 인덱스 연산자([] : array subscript operator or array index operator) 안에 배열 요소의 수가 생략되고 선언된 배열 타입을 말합니다. 크기가 정해진 배열 타입은 complete type인 반면에 크기가 정해지지 않은 배열 타입은 incomplete type입니다. 크기가 정해진 다른 배열만 배열의 요소가 될 수 있으므로 다차원 배열 초기화 식의 경우 항상 첫 번째 배열 크기만 생략될 수 있습니다. 

 

int iaArr[3][3] = {0, };               // OK
int iaArr[][3] = {0, };                 // OK, 첫 번째 배열 크기 생각 가능, 초기화 요소 수에 의해서 배열 크기 정해짐
int iaArr[3][] = {0, };                 // NOK, 첫 번째가 아니면 생략 불가능, compile error 발생
int iaaArr[2][3][4] = {0, };           // OK
int iaaArr[][3][4] = {0, };            // OK, 첫 번째 배열 크기 생각 가능, 초기화 요소 수에 의해서 배열 크기 정해짐
int iaaArr[2][][4] = {0, };            // NOK, 첫 번째가 아니면 생략 불가능, compile error 발생
int iaaArr[2][3][] = {0, };            // NOK, 첫 번째가 아니면 생략 불가능, compile error 발생

 

배열 변수를 지역 변수로 선언만 하고 초기화를 하지 않으면, 일반 변수를 초기화 하지 않은 것과 마찬 가지로 쓰레기 값(garbage)을 가지고 있을 수 있습니다. 모든 변수의 초기화는 자동차에 타면 안전 밸트 매듯이 습관적으로 해야 합니다. 여러가지 자료형의 1차원 배열 선언 및 초기화 방법에 대해서 예제 코드로 알아 보겠습니다.

 

다음은 char 자료형 1차원 배열 예제입니다.

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
#include <stdio.h>
 
int main (void)
{  
    char    cArr1[4= {'H','I','H','I'};       //  char type 배열 (array of [4] char)
    char    cArr2[8= "Hello!";                //  char type 배열, 문자열 리터럴로 초기화  
    
    int i;
    
    for (i = 0; i < 4; i++){
        printf("cArr1[%d] = %2c ", i, cArr1[i]); 
    }
    printf("\n");
    printf("cArr2 = %s \n\n", cArr2);
    
    printf("sizeof \"Hello!\" : %d\n"sizeof ("Hello!"));
    printf("address &\"Hello!\" : %p\n"&"Hello!");
    printf("\"Hello!\"[4] : %c\n\n""Hello!"[4]);
    
    cArr1[4= 'O';                             //  배열 boundary를 벗어 나서 써도 에러 체크 안된다  
 
    for (i = 0; i < 5; i++){
        printf("cArr1[%d] = %2c ", i, cArr1[i]); 
    }
    printf("\n");    
    
    cArr2[6= ' ';
    cArr2[7= 'h';
    cArr2[8= 'i';                             //  배열 boundary를 벗어 나서 써도 에러 체크 안된다  
    cArr2[9= 0;                               //  배열 boundary를 벗어 나서 써도 에러 체크 안된다 
 
    printf("cArr2 = %s \n", cArr2);
    
    return 0;
}
 
-------------------------------------------------------------------------------------------------
 
cArr1[0=  H cArr1[1=  I cArr1[2=  H cArr1[3=  I
cArr2 = Hello!
 
sizeof "Hello!" : 7 // "Hello!"은 char [7] 타입의 배열이다
address &"Hello!" : 0000000000404035 // 재사용 될 문자열 literal은 .rodata 영역에 저장
"Hello!"[4] : o // "Hello!"의 4 번째 요소 o 출력
 
cArr1[0=  H cArr1[1=  I cArr1[2=  H cArr1[3=  I cArr1[4=  O
cArr2 = Hello! hi
cs

참조1. char 자료형 배열의 초기화

 

문자 자료형의 초기화는 참조1의 5라인과 같이 단일 인용부호로 감싼 character로 초기화 리스트 {}를 이용해서 초기화 할 수도 있고, 6라인과 같이 문자열 리터럴을 이용해서 초기화를 할 수도 있습니다.

 

배열 요소(element)에 대한 접근은 index를 이용합니다. 첫 번째 요소는 0부터 시작하고, 마지막 요소는 (배열 크기 - 1) 입니다. Index를 (배열 크기 - 1)이 아닌 배열 크기 그 자체로 접근하면 배열 경계(boundary)를 벗어나 배열 요소가 아닌 메모리를 참조하게 되니, 이 부분은 배열을 사용할 때 무조건 언제나 항상 자나깨나 염두에 두어야 합니다. ^^ 10~12라인과 같이 index 0 ~ 3을 이용해서 크기 4인 문자 자료형 각각의 요소에 접근할 수 있습니다.

 

문자열 리터럴의 자료형(data type)은 char 배열입니다. arr[5]와 같은 배열의 표현 형식은 아니지만, 라인 5에서 문자 자료형 배열의 초기화에 사용한 "Hello!" 이 자체가 배열입니다. 그래서 16 ~ 18 라인에서와 같이 배열의 일반적인 연산을 수행할 수 있습니다. 42 ~ 44 라인의 출력 결과를 보면, "Hello!"가 char [7] type의 문자 자료형 배열의 결과를 출력되는 것을 볼 수 있습니다.

 

20라인과 29 ~ 30 라인을 보면, 배열의 경계(boundary)를 벗어난 곳을 정상적으로 배열 이름을 통해 접근해서, 새로운 값을 쓰고 있습니다. 그렇지만, 컴파일시에도 warning이 발생하지도 않을뿐더러, 46 ~ 47 라인과 같이 새로운 값을 쓴 것이 결과에 그대로 출력되고 있습니다. 이거슨 buffer overflow에 해당하는 bug입니다. 실제 실행 환경에서 이렇게 사용한다면, 어떤 결과가 나올지 모릅니다. 심한 경우, 프로그램이 오동작으로 뻗어 버릴 수도 있습니다. C에서 배열을 사용할 때 배열 요소 접근에 대한 경계 체크는 프로그래머의 책임임을 명심해야 합니다.

 

다음은 short 자료형 1차원 배열 예제입니다.

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
#include <stdio.h>
 
int main (void)
{  
    short   sArr1[4= {1357};                 //  short type 배열 (array of [4] short)
    short   sArr2[]  = {2468};                 //  크기 지정 X, 초기화에 의해 정해짐  
    
    int i;
    
    for (i = 0; i < 4; i++){
        printf("sArr1[%d] = %2d ", i, sArr1[i]);
    }
    
    printf("\n");
    for (i = 0; i < 4; i++){
        printf("sArr2[%d] = %2d ", i, sArr2[i]);
    }
    
    printf("\n");
    printf("sizeof sArr2  = %2d \n"sizeof sArr2);  //  short 형 자료 4개의 크기가 출력된다   
 
    return 0;
}
 
-------------------------------------------------------------------------------------------------
 
sArr1[0=  1 sArr1[1=  3 sArr1[2=  5 sArr1[3=  7
sArr2[0=  2 sArr2[1=  4 sArr2[2=  6 sArr2[3=  8
sizeof sArr2  =  8 //  short 형 자료 4개의 크기 8 출력
cs

참조2. short 자료형 배열의 초기화

 

참조2의 5,6 라인에 short 자료형에 대한 1차원 배열을 선언하고, 선언과 동시에 초기화 리스트를 사용해서 초기화하였습니다. 그런데, 6라인의 sArr2는 [] 안이 비어 있습니다. 즉, 배열 크기가 지정되지 않았습니다. 배열 크기를 지정하지 않는 경우, 배열은 초기화에 사용된 요소의 수로 size가 정해 집니다. 6 라인에서는 short type 요소 4개를 초기화 하였기때문에  20 라인에서 sizeof 로 배열의 크기를 확인했을 때, 29 라인에서 8로 결과가 출력되었습니다.

 

다음은 int, double, struct 자료형 1차원 배열 예제입니다.

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
#include <stdio.h>
 
typedef struct {
    int A;
    int B;
    int C;
    int D;
}structE;
 
int main (void)
{  
    int     iArr1[4];                                  //  int type 배열, 초기화 X 
    double  dArr1[4= {};                             //  double type 배열 (array of [4] double)
    double  dArr2[4= {[1]=5.0, [3]=7.0};             //  double type 배열, 특정 위치 초기화  
    structE eArr1[4= {{1234},{2468}};    //  struct type 배열 (array of [4] structE)
    
    int i;
    
    for (i = 0; i < 4; i++){
        printf("iArr1[%d] = %4d ", i, iArr1[i]); 
    }
    printf("\n");
    for (i = 0; i < 4; i++){
        printf("dArr1[%d] = %.2f ", i, dArr1[i]); 
    }
    printf("\n");
    for (i = 0; i < 4; i++){
        printf("dArr2[%d] = %.2f ", i, dArr2[i]); 
    }
    printf("\n");
    for (i = 0; i < 4; i++){
        printf("eArr1[%d].A = %2d ", i, eArr1[i].A);
        printf("eArr1[%d].B = %2d ", i, eArr1[i].B); 
        printf("eArr1[%d].C = %2d ", i, eArr1[i].C); 
        printf("eArr1[%d].C = %2d \n", i, eArr1[i].D); 
    }
    
    return 0;
}
 
-------------------------------------------------------------------------------------------------
 
iArr1[0=    1 iArr1[1=    0 iArr1[2= 4204169 iArr1[3=    0
dArr1[0= 0.00 dArr1[1= 0.00 dArr1[2= 0.00 dArr1[3= 0.00
dArr2[0= 0.00 dArr2[1= 5.00 dArr2[2= 0.00 dArr2[3= 7.00
eArr1[0].A =  1 eArr1[0].B =  2 eArr1[0].C =  3 eArr1[0].C =  4
eArr1[1].A =  2 eArr1[1].B =  4 eArr1[1].C =  6 eArr1[1].C =  8
eArr1[2].A =  0 eArr1[2].B =  0 eArr1[2].C =  0 eArr1[2].C =  0
eArr1[3].A =  0 eArr1[3].B =  0 eArr1[3].C =  0 eArr1[3].C =  0
cs

참조3. int, double, struct 자료형 배열의 초기화

 

12 라인은 int 자료형 배열 변수를 선언만 하고, 초기화를 하지 않았습니다. 13 라인은 초기화 리스트 안에 값은 없이 초기화를 하였습니다. 이 형식은 gcc에서는 사용될 수 있지만, 표준에서는 허용되지 않습니다. 표준에서는 최소 1개라도 초기화 리스트에 요소가 초기화 되어야만 합니다. 지역 변수로 선언된 배열 변수도 초기화를 하지 않은 경우에는 43라인의 iArr1[2]의 결과 값과 같이 쓰레기 값이 들어 있을 수 있습니다. 반면에 초기화 리스트로 초기화 한 경우에는 특정 값(보통 0)으로 초기화가 됩니다. C 표준에서는 빈 초기화 리스트로 초기화 하는 것이 금지이지만, C++에서는 허용됩니다. 좋은 습관은 빈 초기화 리스트로 초기화 하지 않고, {0,}; 와 같이 첫 번째 요소를 넣어서 초기화 하는 것입니다. 첫 번째, 요소만 0으로 초기화를 하면, 나머지 요소들도 모두 0으로 초기화 됩니다.

 

14 라인은 배열 요소(element) 중 초기화 할 요소만 index(배열에서의 위치를 나타내는 숫자)로 초기화를 하였습니다. 초기화 리스트를 나타내는 중괄호 { } 안에 배열 인덱스 연산자 [] 안에 초기화 하고자 하는 index를 적고 초기화할 값을 대입 연산자로 대입하여 주었습니다. 66라인의 출력 결과를 보면, 초기화 하지 않은 0, 2  index의 요소는 0으로 초기화가 되었습니다.

 

15 라인은 구조체 배열인데요. 앞에 2개 요소만 초기화를 하였습니다.  48, 49 라인을 보면,  부분 초기화를 한 경우에도 초기화 하지 않은 다른  요소들은 모두 0으로 초기화가 된 것을 볼 수 있습니다.

 

다음은 enum 자료형과 char const * 배열 자료형, 그리고 enum days [7] 포인터의 1차원 배열 예제입니다.

 

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
#include<stdio.h>
 
enum days { MON = 0, TUE, WED, THU, FRI, SAT, SUN };
 
int main()
{
    enum days dDays[7= {MON, TUE, WED, THU, FRI, SAT, SUN};
    char const *apDays[] = { "월요일""화요일""수요일""목요일""금요일""토요일""일요일" }; 
    enum days (*paDays)[7= &dDays;
     
    int i;
   
    for(i = MON; i <= SUN; i++){
        printf("하나님께서 %d번째 요일로  %s을 창조 하셨심니더. \n", dDays[i]+1, apDays[dDays[i]]);
    }
    
    printf("\n");
    
    printf("sizeof dDays  = %2d, &dDays  = 0x%08x\n"sizeof dDays, &dDays);
    printf("sizeof apDays = %2d, &apDays = 0x%08x\n"sizeof apDays, &apDays);
    printf("sizeof paDays = %2d, &paDays = 0x%08x\n"sizeof paDays, &paDays);
    printf("pDays[0] = 0x%08x, apDays[6] = 0x%08x\n", apDays[0], apDays[6]);
    
    printf("\n");
    
    for(i = MON; i <= SUN; i++){
        printf("하나님께서 %d번째 요일로  %s을 창조 하셨심니더. \n", paDays[0][i]+1, apDays[paDays[0][i]]);
    }
   
    return 0;
}
 
-------------------------------------------------------------------------------------------------
 
하나님께서 1번째 요일로  월요일을 창조 하셨심니더.
하나님께서 2번째 요일로  화요일을 창조 하셨심니더.
하나님께서 3번째 요일로  수요일을 창조 하셨심니더.
하나님께서 4번째 요일로  목요일을 창조 하셨심니더.
하나님께서 5번째 요일로  금요일을 창조 하셨심니더.
하나님께서 6번째 요일로  토요일을 창조 하셨심니더.
하나님께서 7번째 요일로  일요일을 창조 하셨심니더.
 
sizeof dDays  = 28&dDays  = 0x0062fe00
sizeof apDays = 56&apDays = 0x0062fdc0
sizeof paDays =  8&paDays = 0x0062fdb8
pDays[0= 0x00404000, apDays[6= 0x0040402a
 
하나님께서 1번째 요일로  월요일을 창조 하셨심니더.
하나님께서 2번째 요일로  화요일을 창조 하셨심니더.
하나님께서 3번째 요일로  수요일을 창조 하셨심니더.
하나님께서 4번째 요일로  목요일을 창조 하셨심니더.
하나님께서 5번째 요일로  금요일을 창조 하셨심니더.
하나님께서 6번째 요일로  토요일을 창조 하셨심니더.
하나님께서 7번째 요일로  일요일을 창조 하셨심니더.
cs

참조4. enum 자료형 배열, 포인터 배열, 그리고 배열 포인터의 초기화

 

참조4의 라인 7에 enum days 타입의 배열 변수 dDays 를 선언하고 초기화 하였습니다. enum 자료형의 요소는 int 자료형과 같습니다. 즉, enum 자료형 변수 1개의 size가 int 형 size와 같습니다. dDays는 요소 7개를 가진 int [7]과 size가 같습니다. enum 자료형 배열을 초기화 하는 경우, 자료형이 enum 자료형이 아니더라도 warning도 없이 컴파일이 성공되므로 초기화 시나 사용시에 자료형 범위 밖으로 벗어나지 않도록 주의 해야 합니다. 

 

라인 8은 char const 포인터 배열을 배열 크기를 지정하지 않고 초기화 리스트를 이용해서 초기화 하였습니다. 초기화 리스트의 요소로 사용된 문자열 리터럴들은 .rodata 영역에 배치가 되고, 그 주소가 포인터 배열 요소들에 저장됩니다. 라인 9는 enum days [7] 배열에 대한 배열 포인터를 선언하고 &dDays로 초기화 하였습니다.

 

포인터 배열 변수는 배열 포인터 변수와 생김새가 비슷해서 서로 혼동되기가 매우 쉽습니다. 제대로 차이를 딱 파악하지 않으면, 포인터 학습 하는 것을 더 와따리 가따리 하게 할 수 있는 위험 요소입니다. 오리뎅이가 이 위험 요소는 쓱 제거하도록 하겠습니다. ^^

 

배열 인덱스 연산자 []의 결합 우선 순위가 천상지존의 자리에 있기때문에 식별자와 배열 인덱스 연산자 [] 사이에 괄호가 없으면, 무조건 배열 변수입니다.

식별자 앞에 *가 있고, 이것을 소괄호 () 가 둘러 싸고 있으면, 무조건 포인터 변수입니다.

 

아주 비슷하게 생겼지만, 전혀 다른 그들, 포인터 배열과 배열 포인터를 선언 예제로 비교해 보겠습니다.

 

char const *apArr[10];       // 이 것은 포인터 배열인가? 배열 포인터인가? : [정답] 포인터 배열입니다.
                                   // 식별자 apArr과 [] 사이에 괄호가 없으면 배열

char const (*paPtr)[10];    // 이 것은 포인터 배열인가? 배열 포인터인가? : [정답] 배열 포인터입니다. 
                                  // 식별자 paPtr 앞에 *가 있고, 이 것들을 ()가 감싸고 있으면 포인터

 

'char const *apArr[10]' 과 같이 변수의 식별자의 좌우에 * 와 [] 연산자가 각각 있는 경우, 연산자의 결합 우선 순위가 []가 높기 때문에 apArr[10] 배열이 되고, 배열 요소의 데이터 타입이 'char const *'가 됩니다.  10개의 char const * 배열 요소를 초기화 하기 위해서는 아래 그림2의 포인터 배열과 같이 10개의 문자열을 담고 있는 객체가 있어야 합니다. 1개의 배열 객체10개의 char const 객체, 도합 11개의 객체를 사용합니다.

 

'char const (*paPtr)[10];' 와 같이 변수의 식별자와 *를 소괄호로 묶어 놓은 경우는 paPtr은 포인터 변수가 되고, 포인터가 가리키는 두 번째 객체의 데이터 타입이 'char const [10]' 이 됩니다. 다음 그림2의 아래 부분과 같이 1개의 포인터 객체1개의 배열 객체, 도합 2개의 객체를 사용합니다.

 

그림2. 포인터 배열과 배열 포인터의 객체(object) 수 비교

그림2와 같이 그림을 그려서 포인터 배열배열 포인터를 비교해 보면, 좀 더 명확하게 차이점이 보입니다. 포인터 배열은 포인터 변수 10개가 stack memory 영역에 연속으로 배치됩니다. "참조될 수 있는 방법이 있고, 무한 공간이 아닌 크기가 한정된 저장 공간 덩어리가 object"입니다. 배열은 1개의 object입니다.  배열의 10개의 요소가 포인터이기 때문에 포인터를 초기화 하기 위해서는 10개의 객체를 필요로 합니다. 10개의 문자열 리터럴 객체는 각각각 Text section의 .rodata 영역에 배치되고, 10개의 포인터는 각 문자열 리터럴의 첫 번째 요소의 주소로 초기화 됩니다. 반면에 배열 포인터는 포인터 변수 객체 1개가 stack 영역에 잡히고, char 배열 요소로 char 변수 10개가 .rodata section에 연속으로 배치됩니다. 딱 2개의 객체만 있으면 됩니다.

 

const 키워드로 한정한 object가 .rodata 영역에 배치되는 것에 대해서 왜~~에? 하시는 분은 [오리뎅이의 C 포인터 이야기 - 1] 편을 다시 한번 쓰윽 둘러봐 주세요.

다음은 크기가 정해진 다른 배열을 요소로 가지는 다차원 배열 예제입니다.

 

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
#include <stdio.h>
 
int main(void){
    int iaArr1[2][4= { { 19216805}, { 19216801} }; // [ ]가 2개면, 2차원 배열
    int iaArr2[3][3= { // [ ]가 2개면, 2차원 배열
                           { 147},
                           { 258},
                           { 369
                         };
    int iaArr3[ ][3= { 2242362482510, }; // [ ]가 2개면, 2차원 배열
    int iaaArr1[ ][2][4= { { { 10101111}, { 1012131} }, // [ ]가 3개면, 3차원 배열
                             { { 10101112}, { 1012131} },
                             { { 10101113}, { 1012131} }, 
                           };
    int iaaArr2[3][2][4= { { { 101112}, { 101111} }, // [ ]가 3개면, 3차원 배열
                              { { 101113}, { 101111} }, 
                            };
    int iaaArr3[ ][2][3= { 224326236339, }; // [ ]가 3개면, 3차원 배열
                    
    printf("sizeof iaArr1  : %3d\n"sizeof iaArr1);
    printf("sizeof iaArr2  : %3d\n"sizeof iaArr2);    
    printf("sizeof iaArr3  : %3d\n"sizeof iaArr3);    
    printf("sizeof iaaArr1 : %3d\n"sizeof iaaArr1);    
    printf("sizeof iaaArr2 : %3d\n"sizeof iaaArr2);    
    printf("sizeof iaaArr3 : %3d\n"sizeof iaaArr3);
    
    printf("\n");
    
    int i, j=0, n;
    n = sizeof(iaArr3)/(sizeof(int* 3);
    for(i = 0; i < n; i++){
        printf("%2d x %2d = %2d\n", iaArr3[i][j], iaArr3[i][j+1], iaArr3[i][j+2]);
    }
    
    printf("\n");
    for(i = 0; i < 3; i++){
        printf("List #%d IP = %d.%d.%d.%d, GW = %d.%d.%d.%d\n", i,
                iaaArr2[i][0][0], iaaArr2[i][0][1], iaaArr2[i][0][2], iaaArr2[i][0][3],
                iaaArr2[i][1][0], iaaArr2[i][1][1], iaaArr2[i][1][2], iaaArr2[i][1][3]);                
    }
 
    printf("\n");
    n = sizeof(iaaArr3)/(sizeof(int* 3 * 2);
    for(i = 0; i < n; i++){
        printf("%2d x %2d = %2d, ", iaaArr3[i][0][j], iaaArr3[i][0][j+1], iaaArr3[i][0][j+2]);
        printf("%2d x %2d = %2d\n", iaaArr3[i][1][j], iaaArr3[i][1][j+1], iaaArr3[i][1][j+2]);
    }
 
    return 0;                        
}
 
-------------------------------------------------------------------------------------------------
 
sizeof iaArr1  :  32
sizeof iaArr2  :  36
sizeof iaArr3  :  48
sizeof iaaArr1 :  96
sizeof iaaArr2 :  96
sizeof iaaArr3 :  48
 
 2 x  2 =  4
 2 x  3 =  6
 2 x  4 =  8
 2 x  5 = 10
 
List #0 IP = 10.11.1.2, GW = 10.11.1.1
List #1 IP = 10.11.1.3, GW = 10.11.1.1
List #2 IP = 0.0.0.0, GW = 0.0.0.0
 
 2 x  2 =  4,  3 x  2 =  6
 2 x  3 =  6,  3 x  3 =  9
cs

참조5. 다차원 배열의 초기화

 

다차원 배열이란 2차원 이상의 배열을 의미하며, 배열 요소로 크기가 정해진 다른 배열을 가지는 배열을 의미합니다. 2차원 배열은 크기가 정해진 1차원 배열을 요소로 가지는 배열입니다. 3차원 배열은 크기가 정해진 2차원 배열을 요소로 가지는 배열입니다. 4차원 배열은 크기가 정해진 3차원 배열을 요소로 가지는 배열입니다. N차원 배열은 크기가 정해진 N-1차원 배열을 요소로 가지는 배열입니다.

 

참조5에서는 다차원 배열을 선언과 함께 즉시 초기화 하는 여러가지 방법들을 2차원 배열과 3차원 배열을 이용해서 살펴 보았습니다.  초기화 리스트에 사용하는 중괄호(curly bracket), {} 하나가 배열 차원 1개를 나타내도록 사용됩니다. 아래 그림3의 3차원 배열 예에서와 같이 가장 안쪽의 {}부터 바깥쪽의 {}으로 차원이 올라갑니다. 3차원 배열을 예로 들어 얘기 하자면, 가장 안쪽 {}이 1차원 배열, 그 다음 {}이 2차원 배열, 그 다음 {}이 3차원 배열을 나타냅니다. 더 고차원의 배열이라 하더라도 이 규칙은 다름이 없이 같습니다. 

 

그림3. 3차원 배열의 dimension

그림3에서 보여 지듯이 1차원 배열의 요소만이 lvalue이고, 특정 data type의 value가 될 수 있습니다. 2차원 배열의 요소는 크기가 정해진 1차원 배열입니다. 3차원 배열의 요소는 크기가 정해진 2차원 배열입니다. N차원 배열의 요소는 크기가 정해진 N-1 차원 배열입니다. 배열은 lvalue가 아닙니다. lvalue가 아니니, lvalue로 사용될 수 없고, lvalue conversion이 일어 날 수도 없습니다. 단, 다음 장에서 이야기 하게될 내용이지만, 배열은 lvalue가 아니라서 lvalue conversion은 일어날 수 없는 반면에, pointer conversion이 일어 날 수 있습니다.

 

참조5의 라인 10,11,18을 보면, 2차원 배열과 3차원 배열들이 각각 선언 되었는데, 최고차원의 배열 인덱스 연산자 안에 상수 숫자가 채워져 있지 않고 [ ]로 비워져 있습니다. 위에서도 말씀 드렸지만, 배열 요소로는 크기가 정해진 배열만이 될 수 있기때문에 배열 크기를 생략하고 선언할 수 있는 것은 최고차원만 가능합니다. 이 때, 배열의 전체 크기는 초기화된 요소의 갯수에 의해서 정해집니다. 배열이 sizeof의 피연산로 사용되는 경우는, 아래 배열과 포인터에서 이야기 될 배열이 pointer conversion 되지 않고 사용되는 딱 3가지 case 중에 하나에 해당합니다. 그래서 라인 30과 43에서와 같이 배열의 전체 size를 하위 배열 요소들의 배열크기와 1차원 배열 요소의 데이터 type 크기를 곱한 것으로 나누면 최고차원의 배열 크기를 구할 수 있습니다.

 

int iaArr3[ ][3= { 2242362482510, };  // sizeof (iaArr3) : pointer conversion이 일어나지 않는다
                                                                   // 1차원 배열 크기(3) * sizeof (int) = 1차원 배열 1개의 size
= sizeof(iaArr3)/(sizeof(int* 3);                        // 지정되지 않은 2차원 배열의 크기를 구할 수 있다.

 

참조5의 라인 10과 18은 초기화 리스트 안에서 하위 차원의 배열을 curly bracket으로 구분하지 않고, list로 나열을 하였습니다. 이 경우 나열된 list 요소들은 1차원 배열의 요소 크기부터 상위 차원 배열로 맞추어 가면서 초기화됩니다.

 

int iaaArr3[2][2][3= { 224326236339 };                // curly bracket, {}으로 배열 구분 없이
                                                                                    //  요소들을 list로 초기화한 경우,

int
iaaArr3[2][2][3= { {224}, {326}, {236}, {339} };          // 먼저 1차원 배열 요소 초기화를 하고,

int
iaaArr3[2][2][3= { { {224}, {326} }, { {236}, {339} } };  // 2차원 배열 요소 초기화하는 방식으로
                                                                                     // 상위 차원으로 묶어 간다

 

참조5의 라인 15에서와 같이 배열 요소가 부분 초기화 된 경우, 초기화 하지 않은 나머지 부분의 1차원 배열 요소들은 0으로 초기화 됩니다.

 

 

지금까지 배열의 밥 아조씨, BoB를 살펴 보았습니다. 참 쉽죠? ~~ 잉! 

 

이제 다음 장에서는 포인터와 연관되어 C를 학습하는 데 심각한 에로(?) 사항중의 하나인 배열과 포인터의 관계에 대해서 BoB 아조씨가 울고 갈 정도로 쉽고 쉽게 썰을 풀어 보겠습니다. 채널 고정!!! 큐~~ (이게 아니나?). 이러려고 했으나.... 배열 BoB 아조씨에 지면을 너무 많이 할당한 관계로 배열과 포인터의 관계에 대해서는 다음 시간을 예약하도록 하겠습니다.

 

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

 

2021년 1월 24일 수원에서 [오리뎅이]

[오리뎅이의 C 포인터 이야기 - 3] 오리뎅이 포인터 학습법

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

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

 

 

10년전에 사우디에서 주재원으로 근무할 때, 처음으로 가족 여행 갔었던, 이집트 후루가다의 리조트 방갈로 풍경이에요. 3박4일동안 리조트 방갈로에 묵었었었어요. 5개의 대륙 식당이 있고, 삼시세끼랑 간식까지 다 포함했어서 먹고 놀고, 먹고 놀고 했었었는데, 사진첩 들추어 보니 그때 기억들이 새록 새록 떠오르네요. 첫 가족 해외 여행이었어서 더 기억에 남는 너무 행복했었던 기억이에요. 홍해의 짙푸른 바다 속으로 풍덩 스쿠버 다이빙을 하기도 하고... 아이들 다 크긴 했지만, 다시 또 가족 여행 길에 오를 날이 곧 오겠죠. 그때까지... 공부 열심히... ㅎㅎㅎ

 

포인터를 바라보는 시선

단도 직입적으로다가 C pointer 이야기를 시작해 보겠습니다. 포인터 이렇게 바라보면 매우 쉽게 이해하고, 해석할 수 있습니다. 

 

오리뎅이 포인터 학습법칙 1 : 포인터 변수가 선언되면, 포인터도 하나의 객체(object)로 간주한다.


int *pA;             // 단일 포인터 변수는   * 객체 1개와 pA 객체 1개, 총 2개의 객체가 선언된 것으로 간주한다
int **ppA;          // 이중 포인터 변수는   * 객체 2개와 ppA 객체 1개, 총 3개의 객체가 선언된 것으로 간주한다
                       // 다중 포인터 변수도 동일하게 *을 하나의 객체로 간주한다. 
오리뎅이 포인터 학습법칙 2 : 오른쪽 객체부터 첫번째로 왼쪽으로 순서를 매긴다.
오리뎅이 포인터 학습법칙 3 : 왼쪽을 오른쪽 객체의 type으로 해석한다



int *pA;             // 첫 번째 pA 객체의 타입은 * (int * : pointer to int)), 두번째 * 객체의 타입은 int
int **ppA;          // 첫 번째 ppA 객체의 타입은 * (int ** : pointer to pointer to int), 
                       // 두 번째 * 객체의 타입은 * (int * : pointer to int)

                       // 세 번째 * 객체의 타입은 int

 

그림1. 단일 포인터 변수의 해석법

 

그림2. 이중 포인터 변수의 해석법

 

오리뎅이 포인터 학습법칙 4 : 다중 포인터의 마지막 객체의 타입만 포인터 타입이 아니다.
                                       마지막 객체는 무슨 타입? 포인터 아닌 타입

 

실제로 이중 포인터 이상의 다중 포인터가 실제 실무에서 쓰일 일은 거의 없지만, 만일 쓰인다 하더라도 해석하는 것은 동일한 해석법을 적용하면 됩니다. 아래는 지난 [오리뎅이의 C 포인터 이야기 - 2] 에서 사용했던 예입니다. 다중 포인터의 경우, 매 마지막 객체의 타입만 int이고, 그 이전의 *들과 E는 모두 포인터 타입입니다.

 


int *E;             // pointer to int : pointer object 
1개와 int object 1개

int **E;           // pointer to pointer to int : pointer object 2개와 int object 1개
int ***E;         // pointer to pointer to pointer to int : pointer object 3개와 int object 1개
int ****E;       // pointer to pointer to pointer to pointer to int : pointer object 4개와 int object 1개
int *****E;     // pointer to pointer to pointer to pointer to pointer to int : pointer object 5개와 int object 1개

 

오리뎅이 포인터 학습법칙 5: 포인터 객체의 모습은 *pA 와 같이 변수와 *가 같이 연결되어 나타난다.
 
int *pA;         // 실제 코드 상에 사용될 때 모습,  pA   : 첫 번째 객체,   *pA : 두 번째 객체
int **ppA;      // 실제 코드 상에 사용될 때 모습,  ppA  : 첫 번째 객체,  *ppA : 두번째 객체,  **ppA; 세번째 객체

 

포인터 변수의 모든 객체는 초기화가 된다

일반 변수의 경우는 local에 선언하는 변수는 선언만 했을때는 dummy 값이 들어 있기 때문에, 처음에 값을 쓰는데 사용하는 경우가 아니고 값을 읽어서 사용하는 경우에는 값을 초기화 한 이후에 사용해야 합니다. 그렇지 않으면, dummy 값을 읽어서 사용하게 되기 때문에 의도하지 않은 결과, bug를 양산하게 됩니다. static이나 global 변수 선언의 경우는 메모리의 BSS 영역에 잡히면서 0으로 초기화가 됩니다. 반면에 포인터 변수는 Local이건 static이건 global이건 자우지당간에 선언하면 무조건 초기화를 해야 되유! 초기화가 되는 것을 잘 살펴 보면, 포인터 객체가 어떤 것인지 쉽게 찾을 수 있습니다.  실제 그런지 간단한 예제를 통해서 살펴 보겠어요.

 

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
#include <stdio.h>
 
int main(void)
{
    int iNum  = 100;                    // stack에 int 변수 iNum 선언되고 100으로 초기화  
    int *piNum = &iNum;                 // stack에 int * 변수 piNum 선언되고, &iNum으로 초기화  
 
    printf("&iNum   = %p\n"&iNum );   // iNum stack 주소 출력 
    printf("&piNum  = %p\n"&piNum );      // piNum stack 주소 출력 
    printf("piNum   = %p\n", piNum );   // 첫 번째 객체 piNum은 &iNum으로 초기화 됨   
 
    printf("iNum    = %d\n", iNum );    // 100
    printf("*piNum  = %d\n"*piNum );  // 두 번째 객체 *piNum = iNum, lvalue conversion
    
    *piNum = 10000;                     // 두 번째 *piNum = iNum, lvalue 
    printf("*piNum  = %d\n"*piNum );  // 두 번째 객체 *piNum = iNum, lvalue conversion
    printf("iNum    = %d\n", iNum );    // 10000
 
    return 0;
}
 
----------------------------------------------------------------------------------------
 
&iNum   = 000000000062FE1C   // stack 메모리 영역
&piNum  = 000000000062FE10         // stack 메모리 영역
piNum   = 000000000062FE1C // iNum의 주소로 초기화 되어 &iNum과 같다
iNum    = 100
*piNum  = 100                 // *piNum은 두 번째 객체(iNum) lvalue conversion
*piNum  = 10000 // *piNum은 두 번째 객체(iNum) lvalue conversion
iNum    = 10000 // *piNum이 두 번째 객체 iNum이므로 iNum이 바뀜
 
cs

참조1. 오리뎅이 C 포인터 이야기 첫 번째 예제 코드

 

참조1은 처음 포인터 학습을 위해 아주 흔하게 사용하는 예제 코드입니다. main 함수 내부에 int 타입 변수 iNum과 int * 타입 변수 piNum은 선언하였습니다. iNum은 정수 100으로 초기화 하였고, piNum은 선언과 동시에 &iNum으로 초기화 하였습니다.

 

포인터 변수 piNum의 자료형은 int * 입니다. 이를 우리말로 읽을 때는 보통 "int 포인터"라고 읽습니다. 영어 표현으로는 "pointer to int" 입니다. 직역해 보면, int에 대한 포인터 정도가 될텐데요. "int 포인터"라고 부르는 게 짧고 의미를 그대로 함축한 모습으로 직관적인 것 같습니다.

 

"변수 앞에 &(ampersand)를 붙이면  포인터가 된다"는 내용은 [오리뎅이의 C 포인터 이야기 - 2] 편에서 자세하게 이야기 했었습니다. [오리뎅이의 C 포인터 이야기 - 1] 편에서는 변수들의 주소를 출력해서 어떤 메모리 위치에 배치 되는지 확인해 보기 위해서, printf 문에서 변수 앞에 &를 붙여 주소를 출력하는 데에 사용했었구요. 2편에서는 주소(포인터) 자료형의 크기를 알아 보기 위해서 각각의 자료형 변수들의 자료형 앞에 &를 붙여서 sizeof 연산으로 64bit 운영체제인 오리뎅이 PC에서 주소의 크기가 모두 8 bytes라는 것도 확인해 보았었습니다. 

 

변수 이름 앞에 &(ampersand) 연산자를 붙이면, 연산 결과는 그 변수의 주소(포인터) 타입이 됩니다. 그래서 포인터 변수는 참조1에서 "int *piNum = &iNum;"과 같이 변수의 이름 앞에 &를 붙여 대입 연산으로 초기화 할 수 있습니다. 가장 흔하게 사용되는 포인터 변수의 초기화 방법입니다. 포인터 변수를 초기화 하는 다른 방법들로는 메모리를 동적할당 받아서 할당 받은 메모리의 시작 위치로 초기화 하는 방법과 정수 값 자체를 포인터 타입으로 캐스팅해서 초기화 하는 방법이 있습니다. 다른 초기화 방법들에 대해서는 이후 다른 글에서 자세히 알아 보겠습니다.

 

포인터를 보는 순간 몇 번째 객체에 access 하는 지를 그냥 뭐 딱 안다. 

참조1에서 int 변수 iNum과 포인터 변수 piNum이 main 함수 내부에 local variable로 선언되고 초기화 된 것을 보면서 저희는 그냥 딱 아래 그림3과 같은 이미지를 떠 올릴 수 있지 말입니다. 참조1의 예제 코드를 컴파일해서 실행하면, 실행 환경에서 iNum과 piNum 변수는 그림3의 왼쪽 이미지의 Stack 영역에 iNum과 piNum이 8bytes 공간을 차지하도록 표시되어 있는것 처럼 각각 Stack 영역에 배치가 됩니다.  C 프로그래밍 메모리 Layout에 대해서 깅가밍가 하시는 분은  [오리뎅이의 C 포인터 이야기 - 1] 편을 살짝 읽어 봐 주시~~~소!!

 

그림3. stack에 자리 잡은 변수와 두 번째 객체 *piNum = iNum

iNum과 piNum 각각이 초기화 된 것을 보면 저희는 그림3의 오른쪽과 같이 포인터 변수인 'int *piNum'의 첫 번째 객체 piNum과 두번째 객체 *piNum의 공간적 image를 떠 올려 볼 수 있습니다. 저희는 이제 *를 보면 그게 몇번째 객체이고, 어떤 object(객체)인지를 바로 딱 알 수 있기 때문에 코드에서 어느 위치에서 포인터를 만나도 당황스럽지 않습니다. 정~엉~~말? 참조1의 13 라인에서 *piNum(딱 보면 두 번째 객체 : piNum이 첫 번째, *piNum이 두 번째)이 printf 문에서 사용되었습니다. lvalue conversion이 일어나는 위치에 사용되었으므로 value로 사용됩니다. 두 번째 객체이고, "*piNum = iNum" 이므로 iNum의 value 100이 28번 라인에서 출력된 것을 볼 수 있습니다. 15 라인에서 다시 *piNum이 사용 되었습니다. 그런데 이번에는 대입 연산자 =의 좌항 피연산자로 사용되었습니다. lvalue conversion이 일어나는 위치가 아니므로, 그냥 lvalue로 사용되어 두번째 객체인 *piNum, 즉 iNum의 주소 지정자로 사용되어 iNum 메모리에 10000 을 대입하는데 사용하였습니다.

 

다중 포인터를 만나더라도 사정은 달라지지 않습니다. 저희는 딱 보면, 이게 몇번째 객체를 access 하는 지 그냥 애~~~애~~ 앱니다. ^^.  이중 포인터 예제 하나 더 보시고 가실께요. 이중 포인터 선언하고, 초기화 되는 것을  유심히 살펴 보시고, 코드에서 두번째 객체, 세번째 객체가 각각 어덯게 사용되는지도 딱 보고 판단해 보세요.  ^^ 

 

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
#include <stdio.h>
 
int main(void)
{
    int iNum  = 100, iVal = 200;            // stack에 int 변수 iNum 선언되고 100으로 초기화  
    int *piNum = &iNum;                     // stack에 int * 변수 piNum 선언되고, &iNum으로 초기화
    int **ppiNum = &piNum;                  // stack에 int ** 변수 ppiNum 선언되고, &piNum으로 초기화  
 
    printf("&iNum   = %p\n"&iNum );       // iNum stack 주소 출력 
    printf("&piNum  = %p\n"&piNum );      // piNum stack 주소 출력
    printf("&ppiNum = %p\n\n"&ppiNum );   // ppiNum stack 주소 출력   
    
    printf("piNum   = %p\n", piNum );       // 첫 번째 객체 piNum은 &iNum으로 초기화 됨 
    printf("ppiNum  = %p\n\n", ppiNum );    // 첫 번째 객체 ppiNum은 두번째 객체 주소 &piNum으로 초기화 됨
    
    printf("*ppiNum  = %p\n"*ppiNum );    // 두 번째 객체 *ppiNum = piNum, lvalue conversion
    printf("piNum   = %p\n", piNum );       // 
    printf("**ppiNum = %d\n\n"**ppiNum ); // 세  번째 객체 **ppiNum = *piNum, lvalue conversion
        
    *ppiNum = &iVal; // 이중 포인터는 두 번째 객체를 변경하려고 할때 주로 사용
    
    printf("*ppiNum  = %p\n"*ppiNum );    // 두 번째 객체 *ppiNum = piNum, lvalue conversion
    printf("piNum   = %p\n", piNum );       // piNum은 이중 포인터 변수 ppiNum의 두번째 객체와 같다  
    printf("**ppiNum = %d\n"**ppiNum );   // 세  번째 객체 **ppiNum = *piNum, lvalue conversion
 
    return 0;
}
 
------------------------------------------------------------------------------------------------------
 
&iNum    = 000000000062FE1C
&piNum  = 000000000062FE10
&ppiNum  = 000000000062FE08
 
piNum    = 000000000062FE1C
ppiNum  = 000000000062FE10
 
*ppiNum  = 000000000062FE1C
piNum   = 000000000062FE1C
**ppiNum = 100
 
*ppiNum  = 000000000062FE18
piNum   = 000000000062FE18
**ppiNum = 200
cs

참조2. 이중 포인터는 뭐 별 다르간듀?

 

이중 포인터는 왜 씁니까? 라고 물으~신다면 눈물의 씨앗이라 말하지 마~~요. ^^ 요즘 트로트를 너무 많이 봤더니 후유증이 나타나는 중입니다. 이중 포인터 변수도 그림으로 쓱 싹 그려 보면 요런 이미지가 딱 그려 집니다

 

그림4. stack에 자리 잡은 변수들과 두 번째 객체 *ppiNum과 세 번째 객체 **ppiNum

 

이중 포인터를 쓰는 경우는 대개 세 번째 객체에 접근하려는 목적보다는 두 번째 객체에 접근해서 두 번째 객체의 value(세 번째 객체의 주소)를 변경하려는 경우가 많습니다. 참조2의 16 라인과 22 라인은 이중 포인터 변수인 ppiNum을 통해서 두 번째 객체인 *ppiNum 을 출력해 보고 있습니다. lvalue conversion이 일어나서 두 번째 객체인 piNum의 value인 iNum의 주소 &iNum이 출력됩니다. 31라인, 35라인, 38라인의 출력된 주소가 모두 동일한 것을 볼 수 있습니다. 열 여덟번째 라인과 24 라인은 세 번째 객체인 **ppiNum을 출력하고 있습니다. 20 라인에서 두 번째 객체를 "*ppiNum = &iVal"로 새로운 주소 &iVal로 변경하고 있습니다. 이는 *ppiNum이 두 번째 객체이므로 piNum의 value가 &iNum에서 &iVal로 변경됩니다. 이로 인해서 열 여덟번째 라인과 24 라인에서 출력한 세 번째 객체인 **ppiNum이 100에서 200으로 변경된 것을 확인할 수 있습니다. 이는 세 번째 객체를 변경해서 나타난 결과가 아니라 아래 그림 5와 같이 두 번째 객체를 변경해서 세 번째 객체도 자동 변경 된 것임을 알 수 있습니다.

 

그림5. 이중 포인터에서 두 번째 객체를 바꾸면, 세 번째 객체도 자동빵으로 바뀐다

 

 

포인터가 사용되는 이유 Top 5

포인터 꼭 사용해야 합니까? 포인터 안쓰면 프로그램 못 만듭니까? 물론 고거슨 아닐테지만, C에서는 포인터가 특별한 기능을 가지고 있어서 반드시 사용을 해야 하는 경우도 있고, 사용하는 경우가 사용하지 않는 것보다 훨씬 효율적으로 코딩을 할 수 있는 경우도 있습니다. 다음은 오리뎅이 차트 "포인터가 사용되는 이유 Top 5"입니다.

 

1. 피호출 함수(callee function)에서 호출 함수(caller function)의 local 변수에 access path
2. 자료형의 크기만큼 주소를 반복 증가 또는 반복 감소하기 위한 용도 (like C++ 반복자)
3. 자료구조 linked list (prev, next)
4. Base 주소를 이용한 상대 주소 access (embedded에서 많이 사용)
5. 함수의 포인터

참조3. 포인터가 사용되는 이유 Top 5 (출처 : 오리뎅이 차트)

 

참조3의 출처는 오리뎅이 내 맘대로 차트입니다. 왜냐고 묻지도 따지지도 마시기 바랍니다. ^^. 포인터가 사용되는 이유 Top 5에서 대망의 1위, 예제로 확인 들어갑니다. 이번 예제 코드는 4가지 함수를 main에서 호출하는 코드라서 조금 깁니다. 길지만, 함수 하나 하나를 보면, 어렵지 않은 내용입니다. 위에서부터 함수 하나씩 쭈욱 쭈욱 보시면서 포인터(*)가 나오면, 이거시 어느 객체로 초기화 되었는지를 호출 함수(caller)인 main에서 확인해 보시고, 과연 포인터가 몇 번째 객체에 access 하는 것인지를 딱 보고 딱 맞춰 보세요.  딱 보면 앱~~니다. ^^

 

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
#include <stdio.h>
 
void try_value_swap(int iNum, int iVal)            // 호출 함수(caller)의 local 변수는  
{                                                   // 피호출 함수에서 직접 access 불가
    int temp ;
 
   temp = iNum;
   iNum = iVal;
   iVal = temp;
}
 
void value_swap(int *piNum, int *piVal)             // pointer를 호출 함수의 local 변수에
{                                                   // access 할 수 있는 access path로 이용  
    int temp ;
 
    temp = *piNum; // 두 번째 객체, iNum value를 temp에 저장
    *piNum = *piVal; // 두 번째 객체, iNum = iVal; 과 같음
    *piVal = temp; // 두 번째 객체, iVal = temp; 와 같음
}
 
 
void address_swap(int **ppiNum, int **ppiVal)       // 호출 함수의 단일 pointer 변수에  
{                                                   // access 할 경우에는 이중 pointer 사용 
    int *temp ;
 
    temp = *ppiNum; // 이중 포인터, 두 번째 객체 access
    *ppiNum = *ppiVal; // 두 번째 객체, lvalue = lvalue conversion
    *ppiVal = temp; // 두 번째 객체, piVal = temp; 와 같음
}
 
void value_swap_by_dptr(int **ppiNum, int **ppiVal) // 이중 포인터를 사용하면 호출 함수의  
{                                                   // 일반 변수의 value에도 access 할 수 있다  
    int temp ;                                      // 어떤 특별한 이유가 없는한 이렇게 왜 씀?  
 
    temp = **ppiNum; // 이중 포인터, 세 번째 객체, temp = iNum;
    **ppiNum = **ppiVal; // 세 번째 객체, lvalue = lvalue conversion
    **ppiVal = temp; // 세 번째 객체, iVal = temp; 와 같음
}
 
 
int main(void)
{
    int iNum  = 100, iVal = 200;                                
    
    
    printf("###### try_value_swap() ######\n"); 
    printf("Before : iNum  = %d, iVal = %d\n", iNum, iVal );   
    
    try_value_swap(iNum, iVal);          // 1 일반 변수를 함수 인자로 넘기면 값 복사가 일어 난다
    
    printf("After  : iNum  = %d, iVal = %d\n", iNum, iVal ); 
    
    printf("\n"); 
    
    printf("###### value_swap() ######\n"); 
    printf("Before : iNum  = %d, iVal = %d\n", iNum, iVal ); 
    
    value_swap(&iNum, &iVal);            // 2 주소(포인터)를 인자로 넘기면 주소가 복사된다
    
    printf("After  : iNum  = %d, iVal = %d\n", iNum, iVal );  
 
    printf("\n"); 
               
    printf("###### address_swap() ######\n");
    iNum = 100, iVal = 200;              // iNum, iVal 값 원복  
    int *piNum = &iNum, *piVal = &iVal;
    printf("Before : iNum   = %d, iVal   = %d\n", iNum, iVal );
    printf("Before : *piNum = %d, *piVal = %d\n"*piNum, *piVal );
    printf("Before : piNum  = 0x%p, piVal = 0x%p\n\n", piNum, piVal ); 
    
    address_swap(&piNum, &piVal);        // 3 포인터 변수의 주소를 넘기면 이중 포인터가 된다
    
    printf("After  : iNum   = %d, iVal   = %d\n", iNum, iVal );
    printf("After  : *piNum = %d, *piVal = %d\n"*piNum, *piVal );
    printf("After  : piNum  = 0x%p, piVal = 0x%p\n", piNum, piVal );
 
    printf("\n"); 
               
    printf("###### value_swap_by_dptr() ######\n");
    iNum = 100, iVal = 200;              // iNum, iVal 값 원복
   address_swap(&piNum, &piVal); // piNu, piVal 주소 값 원복
 
    printf("Before : iNum   = %d, iVal   = %d\n", iNum, iVal );
    printf("Before : *piNum = %d, *piVal = %d\n"*piNum, *piVal );
    printf("Before : piNum  = 0x%p, piVal = 0x%p\n\n", piNum, piVal ); 
    
    value_swap_by_dptr(&piNum, &piVal);  // 4 이중 포인터를 넘기면, 세 번째 객체에도 Access 가능
    
    printf("After  : iNum   = %d, iVal   = %d\n", iNum, iVal );
    printf("After  : *piNum = %d, *piVal = %d\n"*piNum, *piVal );
    printf("After  : piNum  = 0x%p, piVal = 0x%p\n", piNum, piVal );
    
    return 0;
}
 
 
------------------------------------------------------------------------------------------------
 
###### try_value_swap() ######
Before : iNum  = 100, iVal = 200
After  : iNum  = 100, iVal = 200
 
###### value_swap() ######
Before : iNum  = 100, iVal = 200
After  : iNum  = 200, iVal = 100
 
###### address_swap() ######
Before : iNum   = 100, iVal   = 200
Before : *piNum = 100*piVal = 200
Before : piNum  = 0x000000000062FE1C, piVal = 0x000000000062FE18 // 주소가 바뀜
 
After  : iNum   = 100, iVal   = 200
After  : *piNum = 200*piVal = 100
After  : piNum  = 0x000000000062FE18, piVal = 0x000000000062FE1C // 주소가 바뀜
 
###### value_swap_by_dptr() ######
Before : iNum   = 100, iVal   = 200
Before : *piNum = 100*piVal = 200
Before : piNum  = 0x000000000062FE18, piVal = 0x000000000062FE1C
 
After  : iNum   = 200, iVal   = 100
After  : *piNum = 200, *piVal = 100
After  : piNum  = 0x000000000062FE18, piVal = 0x000000000062FE1C
 
 
 
cs

참조4. 호출 함수(caller)의 지역 변수에 access하기 위해서 포인터가 사용된다

 

 

참조1의 4가지 함수를 함수 선언과 main에서의 함수 호출을 각각 살펴 보면서 설명을 해 보겠습니다.

 

첫 번째 함수 : try_value_swap(int iNum, int iVal)

    함수 정의 :
             try_value_swap(int iNum, int iVal)          // iNum, iVal은 try_value_swap()의 local 변수
    함수 호출 :
            t
ry_value_swap(iNum, iVal);                     //  iNum, iVal은 main() 의 local 변수

 

try_value_swap 함수에서 iNum의 iVal은 각각 local 변수로 선언됩니다. main 함수에서 함수 호출 시 사용된 iNum과 iValu은 main 함수의 local 변수입니다. main()과 try_value_swap()에서 서로 이름은 같게 사용했지만, 서로 다른 변수입니다. try_value_swap 함수에서 iNum과 iVal의 swap을 시도하지만 이는 try_value_swap 함수의 지역 변수 iNum과 iVal만 서로 swap 될 뿐, main 함수의 iNum과 iVal에는 영향을 미치지 않습니다.

 

두 번째 함수 :  value_swap(int *piNum, int *piVal)

    함수 정의 : 
            value_swap(int *piNum, int *piVal)         // 포인터 변수 piNum = &iNum, piVal = &iVal로 각각 초기화 됨
    함수 호출 : 
           
value_swap(&iNum, &iVal);                      // iNum과 iVal의 주소 &iNum, &iVal 을 인자로 넘김 

 

함수 인자로 주소(포인터)를 넘기고, 함수에서 매개 변수를 포인터 변수로 받으면, 주소가 포인터 변수로 복사되어 포인터 변수가 초기화 됩니다. value_swap 함수에서 local 변수로 선언된 포인터 변수 piNum과 piVal은  각각 main 함수에 선언된 iNum과 iVal의 주소가 복사 되어, &iNum 그리고 &iVal로 초기화가 됩니다. 이거슨 저희가 참조1과 참조2에서 포인터 변수의 초기화에 사용했던 아래 표현식과 동일합니다. 

 

piNum = &iNum;
piVal = &iVal;

 

main 함수의 iNum과 iVal이 각각 포인터 변수 piNum과 piVal의 두 번째 객체로 초기화 되었기 때문에 함수 본문에서 두 번째 객체에 access하면 main 함수의 iNum과 iVal에 access 하게 됩니다.  이렇게 포인터(주소)를 함수 인자와 매개 변수로 사용하면, 피호출 함수에서 호출 함수의 local 영역에 있는 변수에 access 하여 값을 읽거나, 값을 바꿀 수 있는 access path가 만들어 집니다.  오리뎅이 차트 C에서 "포인터를 사용하는 이유 Top 5"의 넘버 원이 바로 이것이었습니다. 함수에 단일 포인터가 사용되었다면, 그거슨 피호출 함수에서 호출 함수의 영역에 있는 일반 변수에 access 하려는 이유로 사용하는 것입니다.

 

세 번째 함수 : address_swap(int **ppiNum, int **ppiVal)

    함수 정의 : 
            address_swap(int **ppiNum, int **ppiVal)     // 이중 포인터 변수 ppiNum = &piNum, ppiVal = &piVal
    함수 호출 : 
            address
_swap(&piNum, &piVal);                      // piNum과 piVal의 주소 &piNum, &piVal 을 인자로 넘김 

 

이중 포인터 대관절 머땀시 쓴답니까? 10중 8,9는 피호출 함수에서 호출 함수의 local 영역에 있는 단일 포인터 변수를 access 하여 값을 읽거나, 값을 바꾸려는 목적으로 이중 포인터를 사용합니다. 함수 매개 변수에 (int **ppiNum,  int**ppiVal) 과 같이 이중 포인터로 선언하고, 함수 본문에서 '*ppiNum = *ppiVal;'과 같이 사용하면, 이중 포인터 변수의 두 번째 객체에 각각 access 합니다. 대입 연산자(=)의 좌항 피연산자로 사용된 *ppiNum은 lvalue로 사용 되어 두 번째 객체인 piNum이 됩니다. 대입 연산자의 우항 피연산자로 사용된  *ppiVal은 역시 두 번째 객체인 piVal이 되는데, lvaule conversion 되어 piVal의 값인 &iVal, 즉 iVal의 주소 값으로 변환됩니다. 

 

그림6. before address_swap()

 

그림7. after address_swap()

실제 iNum의 값과 iVal의 값은 바뀌지 않았지만, 그림7과 같이 piNum과 piVal이 서로 바뀌었기 때문에 ppiNum의 세 번째 객체의 값을 출력해 보면, 값이 swap 된 것처럼 보입니다.  67~69라인과 73~75라인의 출력 결과인 108~110라인과 112~114라인을 보면, iNum과 iVal은 함수 수행 전과 후에 변함이 없습니다. 반면에 *piNum과 *piVal은 출력 전과 후에 값이 서로 바뀌어 출력되는 것을 볼 수 있습니다. 그리고, 그렇게 보이는 이유가 세번째 라인의 출력결과와 같이 piNum과 piVal의 주소가 서로 뒤 바뀌었기 때문임을 알 수 있습니다.

 

네 번째 함수 : value_swap_by_dptr(int **ppiNum, int **ppiVal)

이중 포인터를 함수의 매개 변수로 사용하면, 호출 함수의 local 영역에 있는 두 번째 객체 뿐만 아니라 세 번째 객체에도 접근할 수 있습니다. 네 번째 함수는 함수 본문에서 세 번째 객체에 접근해서 직접 세 번째 객체의 값을 서로 swap 하는 것을 보여 주고 있습니다.  함수 본문에서 '**ppiNum = **ppiVal;' 와 같이 이중 포인터가 사용되어 각각 세 번째 객체에 접근합니다.  직접 세 전째 객체인 iNum과 iVal에 접근하여 서로 swap을 하는 것이기 때문에 첫 번째 함수 value_swap() 과 동일하게 iNum과 iVal이 swap 된 결과가 나오는 것을 볼 수 있습니다. 따라서, 호출 함수의 포인터 변수가 아닌 일반 변수의 값을 access 하고자 하는 경우에는 그냥 단일 포인터를 사용하지, 이중 포인터를 사용할 필요가 없습니다. 이중 포인터를 사용했다는 것은 머니 머니 해도 호출 함수의 local 영역에 선언된 단일 포인터 변수를 access 하고자 할 때 사용하는 경우가 많습니다.

 

 

포인터(*)가 보이면 포인터가 몇개 붙어 있는지에 따라서 딱 이게 몇 번째 객체, 누구를 access 하는 것이구나가 딱 보이시나요? 딱 딱 보이신다고 믿~슙니다.

 

이중 포인터를 사용하는 한가지 예를 더 보고 오늘의 이야기를 마칠까 합니다. C에서 이중 포인터 얘기 하면 절대 빠질 수 없는 존재감 뿜뿜, 바로 바로~~~, linked list의 head 입니다.

 

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
#include <stdio.h>
 
typedef struct _Student Student;
 
struct _Student
{
    char name[20];
    int IdNum;
  int Kor;
  int Eng;
  int Math;
  float Avg;
    Student *next;
};
 
void db_memory_alloc(Student **ppNew)
{
    *ppNew = (Student *)malloc(sizeof(Student));    // 이중 포인터를 사용하여 main 함수 영역에 선언된  pHead를 
}
 
void print_student_data(Student *pNew)
{
    printf("\n================= ID %d : %s's score start =====================\n", pNew->IdNum, pNew->name);
 
    printf("Input student %s's korean score  : %d\n", pNew->name, pNew->Kor );
    printf("Input student %s's english score : %d\n", pNew->name, pNew->Eng );
    printf("Input student %s's math score    : %d\n", pNew->name, pNew->Math );
 
    printf("Input student %s's Average       : %.2f\n", pNew->name, pNew->Avg );
    
    printf("\n================= ID %d : %s's score end   =====================\n", pNew->IdNum, pNew->name);
}
 
void fill_student_data(Student *pNew)
{
    printf("Input student's name: ");
    scanf"%s"&pNew->name );
    printf("Input student's IdNum: ");
    scanf"%d"&pNew->IdNum );
    printf("Input student's korean score: ");
    scanf"%d"&pNew->Kor );
    printf("Input student's english score: ");
    scanf"%d"&pNew->Eng );
    printf("Input student's math score: ");
    scanf"%d"&pNew->Math );
    printf("\n");
    
    pNew->Avg = (pNew->Kor + pNew->Eng + pNew -> Math)/3;
    
    // print_student_data(pNew);    open for debugging purpose
}
 
void insert_data_to_db(Student **ppHead, Student *pNew)
{
    if(*ppHead == NULL){            // 첫 번째 list insert
        pNew->next = NULL;
        *ppHead = pNew;
        return
    }
    
    Student *pTemp = *ppHead;
    Student *pPrev = *ppHead;
    
    while(1){
        if(pTemp->IdNum > pNew->IdNum){
            if(pTemp ==  *ppHead){    // Head 자리에 Insert 
                pNew->next = pTemp;   // Insert할 자료의 next만 head로 바꿔 주면 끝 
                *ppHead = pNew;       // 이중 포인터를 용하여 main 함수에 선언된 pHead를 pNew로 변경 
            }else{                    // middle 위치에  추가 
                pNew->next = pTemp;
                pPrev->next = pNew;
            }
            break
        }else if (pTemp->IdNum < pNew->IdNum){
            pPrev = pTemp;             
            if(pTemp->next == NULL){  // tail에 추가
                pNew->next = NULL;
                pTemp->next = pNew;
                break;
            }
            pTemp = pTemp->next;
        }else{
            printf("Wrong IdNum %d, alread exist!!\n", pTemp->IdNum);
            break;
        }
    }
}
 
    
void display_student_db(Student *pHead){
    printf("\n=======================================================\n");
    printf("Name    IdNum    Kor    Eng    Math    :    Average\n");
    printf("=======================================================\n");
 
    while(pHead != NULL){
 
        printf("%-8s%-9d%-7d%-7d%-8d    %-7.2f\n",pHead->name, pHead->IdNum,
                                                  pHead->Kor, pHead->Eng, pHead->Math, pHead->Avg);
        pHead = pHead->next;
    }
    printf("=======================================================\n");
}
 
 
int main(void)
{
    Student *pHead = NULL;             // main 함수의 local 영역에 포인터 변수 pHead 선언 
    Student *pNew = NULL;              // main 함수의 local 영역에 포인터 변수 pNew 선언
    int Number = 0;
 
    while(1){
        db_memory_alloc(&pNew);         // memory_alloc 함수에서 main 함수 local 영역에 있는 pNew에 
        if(pNew == NULL){               // malloc으로 할당된 주소를 write 할 수 있게 이중 포인터 사용  
            printf("memory allocation fail!!\n");
            return 1;
        }
        
        fill_student_data(pNew);
        
        insert_data_to_db(&pHead, pNew);// memory_alloc 함수에서 main 함수 local 영역에 있는 pHead를   
                                       // insert_data_to_db 함수에서 변경할 수 있도록 이중 포인터 사용  
        Number++;
        if(Number >= 3)
            break;
    }
 
    display_student_db(pHead);
    
    return 0;
}
 
cs

참조5. 이중 포인터를 사용하는 linked list 예제

 

참조5는 학생들의 이름과 번호를 입력하고, 국, 영, 수 점수를 입력하면 평균을 계산하여 DB에 낮은 번호 순서대로 저장하였다가 순서대로 출력해 주는 예제 프로그램입니다. Linked list에서 이중 포인터를 사용하는 예를 보이기 위해서 만들어 본 예제 코드입니다. 복붙해서 실행 시켜 보시고, 데이터를 입력해서 결과가 어떻게 나오는지도 한번 보세요.

 

예제 코드의 두  함수에서 이중 포인터를 사용하고 있습니다. db_memory_alloc() 함수와 insert_data_to_db() 함수에서 각각 사용하고 있습니다. main 함수의 local 영역에 선언한 단일 포인터 변수 pNew와 pHead를 함수에서 access 해서 변경하기 위해서 이중 포인터를 사용하고 있습니다. db_memory_alloc() 함수에서는 18 라인에서 memory를 malloc으로 동적할당해서 *ppNew, 즉, 이중 포인터 변수 ppNew의 두 번째 객체에 write 해 줌으로써 main 함수의 local 변수인 pNew를 초기화 합니다. 

 

insert_data_to_db() 함수에서는 새로운 학생의 data를 IdNum이 작은 순서로 linked list DB에 insert 할 때, pHead의 자리에도 insert할 수 있도록 이중 포인터를 사용합니다. 즉, 참조5의 65~68 라인을 보면, 새로 입력된 학생의 IdNum이 pHead의 IdNum보다 작을 경우, *ppHead 에 pNew를 대입하고, *ppHead를 다음 위치(next)로 바꾸어 주고 있습니다.

 

이번 이야기 [오리뎅이 포인터 학습법]은 여기에서 마치겠습니다. [오리뎅이 포인터 학습법]의 key point는 "포인터가 사용되는 구문을 보면, 딱 몇 번째 객체에 access 하는 것인지를 한눈에 파악하자" 입니다. 오리뎅이가 생각하기에는 포인터 누가 누구를 가리키고, 또 누구는 누구를 가리키고, 이렇게 찾아 찾아 찾아 따라 가는 포인터 학습법은 실체(객체)가 잘 보이지 않아서 직관적이지 않았던 것 같습니다. 이제 저희는 포인터 변수가 나오면, 첫 번째 객체, 두 번째 객체, 세 번째 객체... 등으로 포인터가 access할 수 있는 각 객체들이 무엇인지 확인하고, 코드 본문에서 포인터를 만났을 때, 딱 보면 몇번째 객체에 access 하는 지를 한 눈에 파악할 수 있습니다.  

 

[오리뎅이의 C 포인터 이야기]는 배열, 문자열, 함수 포인터, 복잡한 복합구문으로 이어집니다. 다른 이야기 주제들도 어떻게 설명을 하면 처음 학습 하시는 분들도 무난하게 받아 들이실 수 있을까를 고민 고민한 학습법으로 돌아 오겠습니다. 긴글 끝까지 읽어 주셔서 고맙습니다.

 

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

 

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


[오리뎅이의 C 포인터 이야기 - 2] 포인터는 주소다

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

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

 

포인터를 위한 기초 체력 증강 프로젝트 그 두번째 시간입니다. 오늘은 첫번째로는 주소의 크기와 더하기 빼기 연산에 대해서 알아 보겠습니다. 두번째는 C언어의 object란 무엇인지에 대해서 알아 봅니다. 그리고 마지막으로 C 언언어의 lvalue와 rvalue에 대해서 알아보면서 포인터를 위한 기초 체력 증강 프로젝트 대 단원의 막(?)을 내리겠습니다. ㅋ

 

주소의 크기, 그리고 주소의 더하기 빼기

그럼 첫번짹 주제로 주소의 크기와 더하기 빼기 연산에 대해서 알아 보겠습니다. "포인터는 주소다"라고 했으니 주소의 크기와 더하기 빼기는 곧 포인터의 크기와 더하기 빼기와 같습니다. 머래는 것이여? 말이여 방구여?  이럴때는 "백문이 불여일타" 라 했습니다. ^^

 

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
#include <stdio.h>
 
typedef struct {
    int A;
    int B;
    int C;
    int D;
}structE;
 
int main (void)
{
    char    charA;      //  1 바이트
    short   shortB;     //  2 바이트
    int     intC;       //  4 바이트
    double  doubleD;    //  8 바이트
    structE structE;    //  16 바이트
    int     arrayF[8];  //  32 바이트
 
    printf("\n");
    printf("자료형 크기, 포인터(주소)  크기\n");
    printf("\n");
 
    printf("sizeof charA   = %2ld,   sizeof(&charA)    = %ld\n"sizeof charA, sizeof(&charA));
    printf("sizeof shortB  = %2ld,   sizeof(&shortB)   = %ld\n"sizeof shortB, sizeof(&shortB));
    printf("sizeof intC    = %2ld,   sizeof(&intC)     = %ld\n"sizeof intC, sizeof(&intC));
    printf("sizeof doubleD = %2ld,   sizeof(&doubleD)  = %ld\n"sizeof doubleD, sizeof(&doubleD));
    printf("sizeof structE = %2ld,   sizeof(&structE)  = %ld\n"sizeof structE, sizeof(&structE));
    printf("sizeof arrayF  = %2ld,   sizeof(&arrayF)   = %ld\n"sizeof arrayF, sizeof(&arrayF));
 
    printf("\n");
    printf("포인터(주소) 1 더하기 연산 \n");
    printf("\n");
 
    printf("&charA   = %p, &charA    + 1 = %p\n"&charA,  &charA + 1);
    printf("&shortB  = %p, &shortB   + 1 = %p\n"&shortB, &shortB + 1);
    printf("&intC    = %p, &intC     + 1 = %p\n"&intC,   &intC  + 1);
    printf("&doubleD = %p, &doubleD  + 1 = %p\n"&doubleD,&doubleD + 1);
    printf("&structE = %p, &structE  + 1 = %p\n"&structE,&structE + 1);
    printf("&arrayF  = %p, &arrayF   + 1 = %p\n"&arrayF, &arrayF  + 1);
 
    printf("\n");
    printf("포인터(주소) 10 더하기 연산 \n");
    printf("\n");
 
    printf("&charA   = %p, &charA   + 10 = %p\n"&charA,  &charA   + 10);
    printf("&shortB  = %p, &shortB  + 10 = %p\n"&shortB, &shortB  + 10);
    printf("&intC    = %p, &intC    + 10 = %p\n"&intC,   &intC    + 10);
    printf("&doubleD = %p, &doubleD + 10 = %p\n"&doubleD,&doubleD + 10);
    printf("&structE = %p, &structE + 10 = %p\n"&structE,&structE + 10);
    printf("&arrayF  = %p, &arrayF  + 10 = %p\n"&arrayF, &arrayF  + 10);
 
    printf("\n");
    printf("포인터(주소) 1 빼기 연산 \n");
    printf("\n");
 
    printf("&charA   = %p, &charA   - 1 = %p\n"&charA,   &charA  - 1);
    printf("&shortB  = %p, &shortB  - 1 = %p\n"&shortB,  &shortB  - 1);
    printf("&intC    = %p, &intC    - 1 = %p\n"&intC,    &intC  - 1);
    printf("&doubleD = %p, &doubleD - 1 = %p\n"&doubleD, &doubleD - 1);
    printf("&structE = %p, &structE - 1 = %p\n"&structE, &structE - 1);
    printf("&arrayF  = %p, &arrayF  - 1 = %p\n"&arrayF,  &arrayF  - 1);
 
    return 0;
}
 
 
cs

참조1. 주소의 크기와 더하기 빼기 연산 예제

 

주소의 크기와 더하기 빼기 연산이 결과가 어떻게 나오는지 알아 보기 위해서 참조1 예제를 만들어 봤습니다. 참조1의 12 라인부터 17라인까지를 보면, char 타입부터 short, int, double, struct, array까지 1바이트 크기부터 32바이트 크기까지 자료형들을 선언해 놓았습니다. 16 bytes 자료형은 4 bytes 크기의 int 형 멤버 변수를 4개 가지는 struct 변수를 선언했고, 32 bytes 크기의 자료형은 int 형 요소를 8개 가지는 배열 변수를 선언했습니다. 변수들은 모두 main 함수의 내부에 선언이 되어 있습니다. [오리뎅이의 C 포인터 이야기 - 1]을 읽고 오신 분이시라면, 각 변수들이 stack 영역에 배치될 것이라는 것을 "척 보면 앱~니다!". 

 

"척보면 앱~니다"라는 말은 황기순이라는 개그맨이 1980 년대에 "청춘만만세(청춘행진곡)"라는 프로를 통해서 유행시켰던 유행어입니다. 30년도 더 지난 시절의 유행어를 사용하였네요. 공감해 주실 독자분이 과연 있으셨으려나요? ^^

 

변수 앞에 &(ampersand)를 붙이면  포인터가 된다

참조1에서 23라인과 28라인까지는 각 자료형의 크기를 출력해 보기 위해서 각 자료 타입의 sizeof 연산 결과를 출력함과 동시에 각 자료 타입의 주소(포인터)의 크기를 출력해 보기 위해서 각 변수 앞에 &(ampersand)를 붙여서 sizeof 연산 결과로 출력하였습니다. 변수 앞에 단항 연산자 &를 붙이면, 연산 결과는 변수가 배치되어 있는 메모리의 주소가 됩니다. 첫번째 부분의 출력 결과를 먼저 살펴 보면 참조2와 같습니다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
root@n1-duck01:~/limcs/c# gcc -o msize.o mem_size.c
root@n1-duck01:~/limcs/c# ./msize.o
 
자료형 크기, 포인터(주소)  크기
 
sizeof charA   =  1,   sizeof(&charA)    = 8
sizeof shortB  =  2,   sizeof(&shortB)   = 8
sizeof intC    =  4,   sizeof(&intC)     = 8
sizeof doubleD =  8,   sizeof(&doubleD)  = 8
sizeof structE = 16,   sizeof(&structE)  = 8
sizeof arrayF  = 32,   sizeof(&arrayF)   = 8
 
cs

참조2. 자료형의 크기와 자료형 주소(포인터)의 크기

 

참조2의 출력 결과에 보여 지듯이 각 자료형 변수의 크기는 알고 있던 그대로 각 자료형 변수의 크기와 동일하게 출력되었습니다. 자료형의 크기는 플랫폼에 따라 다를 수 있기 때문에 "이것이 정답이다"라고는 말할 수 없습니다. 

 

각 자료형 변수의 크기가 1, 2, 4, 8, 16, 32로 각각 서로 다른것에 비해서 각 자료형의 주소(포인터) 크기는 너도 나도 다 동일하게 8입니다. 모든 자료형 변수에 대해서 주소(포인터)의 크기는 모두 같습니다. 단, 플랫폼이 32bits 플랫폼인 경우에는 주소(포인터)의 크기는 4 bytes, 64bits 플랫폼인 경우에는 주소의 크기가 8 bytes입니다. 제 PC는 64bits 플랫폼이기 때문에 주소의 크기가 모두 동일하게 8로 출력 되었습니다.

 

비록 크기는 모두 같지만, 주소(포인터) 변수의 타입은 모두 다음 참조3과 같이 제각각 다릅니다.  * 라는 것은 같지만 그 포인터(주소)에 들어 있는 자료형의 타입이 다르기 때문에 다릅니다.

 

1
2
3
4
5
6
   &charA    : char  * 타입
   &shortB   : short  * 타입
   &intC     : int  * 타입
   &double   : double  * 타입
   &structE  : structE * 타입
   &arrayF   : int (*)[8] 타입 
cs

참조3. 각 자료형 변수 주소(포인터)의 타입

 

정수형 변수의 1더하기는 정수 1 증가, 주소형 변수의 1 더하기는?

두번째와 세번째 예제의 출력은 각 주소(포인터)의 1 더하기 연산과 10 더하기 연산 결과입니다. 출력 결과를 살펴 보기 전에 주소(포인터)의 연산에 대해서 잠시  알아 보고 가야겠습니다.

 

  - 주소(포인터)의 연산은 정수 더하기와 빼기만 지원됩니다. 곱하기와 나누기 연산은 지원되지 않습니다.

        &intC + 1;              // OK, 정수 더하기 OK

        &intC - 20;             // OK, 정수 빼기 OK

        &doubleD + 1.5      // NOK, 정수 아님

        &charA * 3             // NOK, 곱하기 안됨

        &shortB  / 2           // NOK, 나누기 안됨

  - 주소에 정수 더하기와 빼기만 될 뿐, 주소와 주소의 더하기, 빼기는 지원되지 않습니다.

        &intC + 10;            //  OK, 정수 더하기 OK

        &intC + &intC1       //  NOK, 주소 + 연산자의 피연산자로는 정수만 가능

  - 주소(포인터)의 더하기와 빼기 연산의 결과값은 주소입니다.

 

자 이제 참조1의 예제 프로그램을 실행하면 어떤 결과가 나올지 예상이 되시죠? 과연 예상하고 계시는 것과 같은 결과가 나오는지 한번 보시죠.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
포인터(주소) 1 더하기 연산
 
&charA   = 0x7ffe9dacb921&charA    + 1 = 0x7ffe9dacb922   //  1 증가
&shortB  = 0x7ffe9dacb922&shortB   + 1 = 0x7ffe9dacb924   //  2 증가
&intC    = 0x7ffe9dacb924&intC     + 1 = 0x7ffe9dacb928   //  4 증가
&doubleD = 0x7ffe9dacb928&doubleD  + 1 = 0x7ffe9dacb930   //  8 증가
&structE = 0x7ffe9dacb930&structE  + 1 = 0x7ffe9dacb940   // 16 증가
&arrayF  = 0x7ffe9dacb940&arrayF   + 1 = 0x7ffe9dacb960   // 32 증가
 
포인터(주소) 10 더하기 연산
 
&charA   = 0x7ffe9dacb921&charA   + 10 = 0x7ffe9dacb92b   //  10 증가, 0x00a
&shortB  = 0x7ffe9dacb922&shortB  + 10 = 0x7ffe9dacb936   //  20 증가, 0x014
&intC    = 0x7ffe9dacb924&intC    + 10 = 0x7ffe9dacb94c   //  40 증가, 0x028
&doubleD = 0x7ffe9dacb928&doubleD + 10 = 0x7ffe9dacb978   //  80 증가, 0x050
&structE = 0x7ffe9dacb930&structE + 10 = 0x7ffe9dacb9d0   // 160 증가, 0x0a0
&arrayF  = 0x7ffe9dacb940&arrayF  + 10 = 0x7ffe9dacba80   // 320 증가, 0x140
 
cs

참조4. 주소의 1 더하기 연산 결과 그리고 10 더하기 연산 결과

 

주소의 1 더하기 연산 결과는 각 자료형의 크기만큼 주소가 증가하였습니다. 즉, 1바이트 크기의 char 자료형 변수의 주소는 1바이트만큼 증가하고, short 자료형 변수의 주소는 2바이트, int 자료형 변수의 주소는 4 바이트, double 자료형 변수의 주소는 8 바이트 증가하였고, structE와 arrayF는 각각 16 바이트와 32바이트가 증가하였습니다. 아래 그림1은 각 자료형의 포인터 1 더하기 연산 시 주소가 얼마씩 증가하는 지를 보여 주고 있습니다.

 

그림1. 자료형 별 주소(포인터)의 1 더하기 연산 시 증가 바이트

 

주소의 10 더하기 연산 결과는 1 더하기 연산 결과에 비추어 예상되는 결과와 정확히 일치합니다. char 자료형 변수의 주소는 10 바이트 증가하였고, int 자료형 변수의 주소는 40 바이트가 증가하는 등 정확히 자료형의 크기에 10을 곱한 것만큼 증가한 것을 알 수 있습니다.

 

1 더하기와 10더하기 연산 출력 결과를 통해서 예상되는 바와 같이 주소(포인터)의 연산은 자료형의 크기가 1 unit 입니다. 주소의 더하기 연산은 (더하기한 숫자 * 자료형의 크기) 만큼 주소가 증가합니다. 

 

주소의 빼기 연산은 어떨까요. 빼기는 당연히 더하기 연산의 반대 결과로 (빼기한 숫자 * 자료형의 크기) 만큼 주소가 감소합니다. 1 빼기 연산 출력 결과를 통해서 실제로 그런 결과가 나왔는지 보시죠!!

 

1
2
3
4
5
6
7
8
9
포인터(주소) 1 빼기 연산
 
&charA   = 0x7ffe9dacb921&charA   - 1 = 0x7ffe9dacb920   //  1 감소
&shortB  = 0x7ffe9dacb922&shortB  - 1 = 0x7ffe9dacb920   //  2 감소
&intC    = 0x7ffe9dacb924&intC    - 1 = 0x7ffe9dacb920   //  4 감소
&doubleD = 0x7ffe9dacb928&doubleD - 1 = 0x7ffe9dacb920   //  8 감소
&structE = 0x7ffe9dacb930&structE - 1 = 0x7ffe9dacb920   // 16 감소
&arrayF  = 0x7ffe9dacb940&arrayF  - 1 = 0x7ffe9dacb920   // 32 감소
 
cs

참조5. 주소(포인터)의 1 빼기 연산 결과 

 

참조5의 주소(포인터)의 1 빼기 결과는 참 재미나게도 주소가 모두 동일한 주소(0x7ffe9dacb920)를 출력하고 있습니다. 1 바이트 크기의 char 변수의 주소 1 빼기는 1 바이트가 감소하고, short 변수의 주소 1 빼기는 2 바이트가 감소하고, int 형은 4 바이트 감소, double 형은 8 바이트 감소, structE와 arrayF는 각각 16 바이트, 32 바이트씩 감소한 것을 볼 수 있습니다. 각 자료형의 크기 만큼 감소한 것인데요. 이렇게 더하기와 마찬가지로 주소(포인터)의 정수 빼기 연산은 포인터 변수의 자료형의 크기가 감소 단위(unit)입니다. 이렇게 주소의 정수 빼기 연산을 배우고  난 후에, 주소(포인터)의 정수 빼기를 하면 덜 헷갈리는데요. 주소와 주소간 빼기를 할 때, 이게 좀 혼동되기 쉽습니다. 예를 들면 다음과 같은 경우입니다.

 

int iArr[4] = {1, 2, 3, 4};
int *piArr0 = &iArr[0];                                          // piArr0는 배열 iArr의 첫 번째 요소 주소
int *piArr2 = &iArr[2];                                          // piArr2는 배열 iArr의 두 번째 요소 주소 = 첫 번째 요소 주소 + 8 

printf("piArr2 - piArr0 = %d", piArr2 - piArr0);  // 출력 결과가 2 가 나오면 "어!! 2가 거기서 왜 나와?" 하게 된다.

참조6. (주소 - 주소)의 결과는 C 입문자의 상습 혼란 유도기

 

참조6과 같이 주소에서 주소를 빼는 연산의 결과는 주소의 자료형 크기가 unit 1이 되는데, 주소 크기만큼 감소할 것으로 예상해서 혼란에 빠지는 경우가 많습니다. 참조6의 경우 주소로는 int 자료형 크기 4의 2배인 8이 차이가 나므로 빼기 연산에서도 8을 예상했다가, 2가 나오면 "어!! 2가 거기서 왜 나와???" 하고 멘붕에 빠질 수 있습니다.

 

참조5의 코드 예제에서는 스택 메모리 영역에 각 변수가 배치될 때, 배치된 주소가 절묘하게 1, 2, 4, 8, 16, 32 로 배수로 증가해서 주소의 정수 1 빼기 연산의 결과가 모두 동일한 주소를 가리키는 결과가 나왔습니다. 그림2는 각 자료형의 포인터 1 빼기 연산 시 주소가 얼마씩 감소하는 지를 보여 주고 있습니다.

 

그림2. 자료형 별 주소(포인터)의 1 빼기 연산 시 감소 바이트

 

주소 10 빼기 연산 결과는 왜 없냐구요? ^^ 한번 직접 수정해서 확인해 보세요. 자료형 크기의 곱하기 10을 한 것만큼 주소가 감소하는 것을 직접 확인해 보세요.

 

 C에는 object가 (있다, 없다)

C++이나 자바, 그리고 파이썬에는 class(클래스)가 있습니다. 드라마 제목 "이태원 클라쓰"에 나오는 클라쓰는 클래스의 사투리 버전일까요? ^^ 암튼 그 클라쓰가 클래스이고 영어 표기로는 class로 같습니다. C 언어에는 class가 없습니다. Class가 있고 없고에 따라서 C++, 자바, 그리고 파이썬과 같은 class가 있는 언어들은 객체(object) 지향 언어라고 부르고, class가 없이 함수가 절차(순서)대로 실행되는 C 언어는 절차 지향 언어라고 부릅니다. 그렇습니다. C는 객체(object) 지향 언어는 아닙니다. 그렇지만 C에도 object가 있습니다. 그런데, C에서 object는 class 로 대변되는 객체 지향 언어에서 사용되는 그 객체(object)와는 다른 의미로 사용됩니다. C11 표준에서 object에 대한 정의는 다음과 같습니다. 

 

3.15
1 object
region of data storage in the execution environment, the contents of which can represent values
2 NOTE When referenced, an object may be interpreted as having a particular type; see 6.3.2.1.

출처 :  C11 ISO/IEC 9899:201x 표준 문서 3.15

참조6. C의 object에 대한 정의

 

Object는 C 프로그램 실행 환경의 데이터 저장 영역. 구글 번역기의 해석입니다. [오리뎅이의 C 포인터 이야기 - 1] 편에서 C 프로그래밍 메모리 레이아웃에 대해서 알아 보았었는데요. C 프로그램이 실행되는 환경에서 각종 변수와 상수 등이 메모리의 어느 위치에 배치되는지를 알아 보았었습니다. 메모리에 저장된 변수와 상수의 저장 영역도 object입니다. 영역(region)이라는 말에는 연속된 저장공간 덩어리의 개념이 있습니다. 참조될 수 있는 방법이 있고, 무한 공간이 아닌 크기가 한정된 저장 공간 덩어리가 object입니다. 위에서 참조1의 예제를 이용해 charA, shortB, intC, doubleD, structE, arrayF의 각 변수가 실행환경에서 stack 메모리의 어느 주소에 저장되며,  각각의 변수의 크기는 얼마인지를 알아 보았었습니다. 저희가 확인해 보았던 그 각 변수들의 메모리 영역(주소와 크기)C에서 말하는 object입니다. 변수나 상수들만이 object인 것은 아닙니다. C 프로그램 실행 환경에서 데이터의 저장소(storage)는 메모리(RAM) 이외에도 Flash Memory, ROM도 있고, 각종 주변 장치 디바이스들이 자체적으로 가지고 있는 register, buffer, fifo 등의 메모리 공간도 있습니다. 이름, 참조 주소, index 등으로 접근할 수 있고, 크기가 정해져 있으면 object입니다

 

C 언어에서 포인터가 전지전능한(?) 이유가 바로 이 모든 종류의 object를 포인터로 접근할 수 있기때문입니다. 각종 디바이스 메모리나 FPGA, ASIC 등의 register에도 address만 연결되면 포인터를 이용해서 디바이스의 메모리나 reginster에 데이터를 쓰거나, 값을 읽어 올 수 있습니다. 

 

object 설명에 대한 NOTE 끝부분에 "6.3.2.1을 참조하십시오." 라고 적혀 있어서 6.3.2.1도 가져와서 구글 번역기를 돌려 보았습니다. 내용이 좀 많은데다가 문장 구조가 좀 복잡해서 구글님 번역만으로는 이해하기가 상당히 난해 했었습니다. 조금씩 의역을 넣어서 수정을 해 보았습니다.  포인터를 잘 이해하기 위해서 아래 번역해 놓은 내용을 모두 이해해야 하는 것은 아닙니다. 그냥 쓱 한번 읽어 보시기만 하고 지나 가시면 됩니다. 

 

6.3.2 Other Operands
6.3.2.1 Lvalues, arrays, and function designators
1 An lvalue is an expression (with an object type other than void) that potentially designates an object;64) if an lvalue does not designate an object when it is evaluated, the behavior is undefined. When an object is said to have a particular type, the type is specified by the lvalue used to designate the object. A modifiable lvalue is an lvalue that does not have array type, does not have an incomplete type, does not have a const qualified type, and if it is a structure or union, does not have any member (including, recursively, any member or element of all contained aggregates or unions) with a const qualified type.

1 lvalue는 잠재적으로 객체(object)를 지정하는 표현식 (void 이외의 객체 유형)입니다 .64) lvalue가 평가(evaluation) 될 때 객체(object)를 지정하고 있지 않으면 정의되지 않은 동작(undefined behavior)입니다. 객체(object)가 특정 유형(type)을 갖는다 고 할 때 유형(type)은 객체(object)를 지정하는 데 사용되는 lvalue에 의해 지정됩니다. 수정 가능한 lvalue는 배열 type이 아니고 불완전한 type(크기가 정해지지 않은 배열)이 아니고, const 한정된 type이 아니고, 구조체 또는 공용체 경우에는 const로 한정된 멤버를 가지지 않습니다 

2 Except when it is the operand of the sizeof operator, the unary & operator, the ++ operator, the -- operator, or the left operand of the . operator or an assignment operator, an lvalue that does not have array type is converted to the value stored in the designated object (and is no longer an lvalue); this is called lvalue conversion.
If the lvalue has qualified type, the value has the unqualified version of the type of the lvalue; additionally, if the lvalue has atomic type, the value has the non-atomic version of the type of the lvalue; otherwise, the value has the type of the lvalue.
If the lvalue has an incomplete type and does not have array type, the behavior is undefined. If the lvalue designates an object of automatic storage duration that could have been declared with the register storage class (never had its address taken), and that object is uninitialized (not declared with an initializer and no assignment to it has been performed prior to use), the behavior is undefined.

2 sizeof 연산자의 피연산자, 단항 & 연산자의 피연산자, ++ 연산자의 피연산자, --연산자의 피연산자 또는 . 연산자 또는 할당(=, += 등) 연산자좌항 피연산자를 제외하고, 배열 type이 아닌 lvalue는 지정된 객체(object)에 저장된 값으로 변환되며 더 이상 lvalue가 아닙니다. 이것을 lvalue 변환(lvalue conversion)이라고합니다.
lvalue가 한정된 type을 가지면(int const * ptr) vlaue는 lvalue type의 한정되지 않은 버전을 갖습니다. 또한, lvalue가 atomic type을 갖는 경우(int _Atomic *ptr) 값은 lvalue type의 non atomic 버전을 갖습니다. 그렇지 않으면 값은 lvalue의 type을 갖습니다. lvalue에 불완전한 type(int arr[])이 있고 배열 유형이 없으면 정의되지 않은 동작입니다.
lvalue가 레지스터 스토리지 클래스로 선언 될 수있는 자동 저장 기간의 객체를 지정하고 (그 주소를 사용하지 않은 경우) 해당 객체가 초기화되지 않은 경우 (이니셜 라이저로 선언되지 않고 사용 전에 할당되지 않은 경우) ), 동작이 정의되지 않았습니다.

3 Except when it is the operand of the sizeof operator or the unary & operator, or is a string literal used to initialize an array, an expression that has type ‘‘array of type’’ is converted to an expression with type ‘‘pointer to type’’ that points to the initial element of the array object and is not an lvalue. If the array object has register storage class, the behavior is undefined.

sizeof 연산자 또는 단항 & 연산자의 피연산자 이거나 배열을 초기화하는 데 사용되는 문자열 리터럴 인 경우를 제외하고, "array of type'' type을 가진 표현식 배열 객체(object)의 첫번째 요소를 가리키는 ''pointer to type" 의  type을 가진 표현식으로 변환(conversion)되고, lvalue가 아닙니다. 배열 객체가 레지스터 스토리지 클래스를 가지면 동작이 정의되지 않습니다.

4 A function designator is an expression that has function type. Except when it is the operand of the sizeof operator65) or the unary & operator, a function designator with type ‘‘function returning type’’ is converted to an expression that has type ‘‘pointer to function returning type’’.

 4 함수 지정자(함수 이름)는 함수 type을 가지는 표현식입니다. sizeof 연산자 65) 또는 단항 & 연산자의 피연산자 인 경우를 제외하고‘‘ type을 반환하는 함수’’ type의 함수 포인터는 ‘‘type을 반환하는 함수에 대한 pointer’’ type을 가지는 표현식으로 변환됩니다.


64) The name ‘‘lvalue’’ comes originally from the assignment expression E1 = E2, in which the left operand E1 is required to be a (modifiable) lvalue. It is perhaps better considered as representing an object ‘‘locator value’’. What is sometimes called ‘‘rvalue’’ is in this International Standard described as the ‘‘value of an expression’’.
An obvious example of an lvalue is an identifier of an object. As a further example, if E is a unary expression that is a pointer to an object, *E is an lvalue that designates the object to which E points.



64) 이름‘‘lvalue’’는 원래 할당 표현식 E1 = E2에서 유래되었으며, 여기서 왼쪽 피연산자 E1은 (수정 가능한) lvalue 여야합니다. 아마도 객체(object) '‘로케이터 값 : 위치를 찾아내는 값’’을  나타내는 것으로 간주하는 것이 좋습니다. 때때로‘‘rvalue’’라고 하는 것은‘‘표현식의 값:value of an expression’’으로 설명되는 국제 표준에 있습니다.
lvalue의 명백한 예는 객체(object)의 식별자(변수 이름)입니다. 추가적인 예로 어떤 객체(object)에 대한 포인터인 E가 단항 표현식에 있는 경우, *E는 E가 가리키는 객체(object)를 지정하는 lvalue입니다


65) Because this conversion does not occur, the operand of the sizeof operator remains a function designator and violates the constraint in 6.5.3.4.

65) 이 변환이 발생하지 않기 때문에 sizeof 연산자의 피연산자는 함수 지정자로 남아 6.5.3.4의 제약 조건을 위반합니다.


출처 : C11 ISO/IEC 9899:201x 표준 문서 6.3.2.1

참조2.  피연산자 lvalues, arrays 그리고 function designators

 

lvalue vs rvalue

C11 표준의 6.3.2.1 항의 주석 64)에 lvalue와 rvalue에 대한 설명이 나옵니다. E1 = E2에서 유래 되었다고 설명하고, 왼쪽 피연산자 E1은 (수정 가능한) lvalue 여야 한다고 설명하고 있습니다. 객체(object)의 위치를 찾아내는 값(locator value)으로 간주하는게 좋겠다고 설명합니다. 결정적으로 밑부분에 "lvalue의 명백한 예는 객체(object)의 식별자(변수 이름)입니다.". 변수 이름 자체가 명백한 lvalue의 예라고 하는군요. 6.3.2.1의 2항 부분을 보면, lvalue가 아니라 lvalue가 가리키는 객체(object)에 저장된 value로 변환되는 lvalue conversion에 대해서 설명하고 있습니다. 2항 앞 부분에 lvalue 변환이 일어나지 않는 예외 경우들에 대해서 열거하고 있습니다. 이해를 돕기 위해서 예제를 포함하여 아래에 다시 열거해 보았습니다.

 


- sizeof 연산자의 피연산자
   printf("%d", sizeof intC);      // siozeof 피연산자 intC는 lvalue 유지

- 단항 & 연산자의 피연산자
   printf("%p", &intC);            // & 피연산자 intC는 lvalue 유지

- ++ 연산자의 피연산자
   ++intC;                           // 전위 연산자 ++ 피연산자 intC는 lvalue 유지
    shortB++;                       // 후위 연산자 ++ 피연산자 shortB는 lvalue 유지

- --연산자의 피연산자
   --intC;                             // 전위 연산자 -- 피연산자 intC는 lvalue 유지
   shortB--;                          // 후위 연산자 -- 피연산자 shortB는 lvalue 유지

- . 연산자의 좌항 피연산자
    structE.A = 100;                // 구조체 변수의 멤버 면수 접근 연산자 . 의 좌항 구조체 변수는 lvalue 유지

- 할당 연산자(=, += 등)의 좌항 피연산자
    intC = 10;                        // 대입 연산자의 좌항 피연산자 intC는 lvalue 유지

- 배열 type
    printf("%p", arrayE);           // int arrayE[8]  arrayE는 lvalue 유지

참조3.  lvalue conversion이 일어 나지 않는 경우의 예

 

위에 언급된 것들과 다르게 사용되는 lvalue object는 lvalue conversion이 되서 object에 저장된 value로 변환된다는 것인데요. 정말 그런지 간단한 예를 들어서 확인해 보겠습니다.

 

int intC, intC1 = 10, intC2 = 20;
intC = intC1 + intC2;                         // intC는 = 연산자 좌항 피연산자이므로 lvalue 유지
                                                    // intC1, intC2는 lvalue conversion 되어, value 10과 20이 됨
printf("%d, %d, %d", intC,intC1,intC2);    // intC, intC1, intC2 모두 lvalue conversion 되어 30,10,20이 됨

 

참조3을 몇번 보고 고개를 끄덕 끄덕 하고 나면, object의 식별자가 lvalue로 사용되는 경우와 lvalue conversion이 되어 value로 사용되는 경우를 어렵지 않게 구분할 수 있을 것 같습니다. ^^

 

*E는 E가 가리키는 객체(object)를 지정하는 lvalue다

참조2에서 주석 64)의 붉은색 부분은 포인터의 정체성에 대해서 설명해 주고 있습니다. 이 부분이 포인터를 이해하기 쉽고, 해석하기 쉽게 해 줄 key point 인 것 같습니다. *E에서 E자신도 pointer 객체(object)의 lvalue이고, *E도 E가 가리키는 객체(object)의 lvalue입니다. int *E의 포인터 변수를 선언하는 순간, 2개의 객체(object)와 2개의 lvalue가 선언된 것입니다. 이중 포인터로 확장해서 생각해 보면, int **E의 이중 포인터 변수를 선언하는 순간, 3개의 객체와 3개의 lvalue가 선언된 것입니다.  E는 포인터 객체이고, *E도 E가 가리키는 포인터 객체이고, **E는 *E가 가리키는 int 객체입니다. 다중 포인터로 확장해도 이것은 마찬가지입니다. int *****E와 같이 선언한다면 별(*)이 5개이니 포인터 객체가 5개이고, int 객체가 1개로 6개의 객체가 선언된 것입니다. 변수를 초기화 하지 않고 사용하면 안되듯이, 포인터 변수 선언 시 선언된 모든 객체는 초기화를 하고 사용해야 합니다.

 

int *E;              // pointer to int : pointer object 1개와 int object 1개
int **E;            // pointer to pointer to int : pointer object 2개와 int object 1개
int ***E;          // pointer to pointer to pointer to int : pointer object 3개와 int object 1개
int ****E;        // pointer to pointer to pointer to pointer to int : pointer object 4개와 int object 1개
int *****E;      // pointer to pointer to pointer to pointer to pointer to int : pointer object 5개와 int object 1개

 

변수가 lvalue로 사용되는 경우와 lvalue coversion이 일어나는 것과 같이 pointer 객체들도 동일하게 lvalue 입니다. lvalue 자리에서는 lvalue로 사용되고 lvalue conversion이 일어나는 곳에서는 value로 사용됩니다. * 하나 하나를 객체 하나로 count 하는 것이 포인터를 쉽게 이해하는 신의 한수입니다. 포인터 변수를 선언하면 포인터 변수가 가리키는 객체들도 초기화가 되어야만 문제 없이 사용될 수 있습니다. 따라서 다중 포인터가 선언되더라도 *에 해당하는 객체가 어느 것인지를 아는 것은 어렵지 않게 파악할 수 있습니다.  아래의 예를 한번 보시죠.  

 


int  D1 = 10, D2;
int  *E1 = &D1, *E2;            // int *E1, *E2는 pointer object와 int object 도합 2개 object    
int  **F1 = &E1, **F2 = &E2; // int **F1, **F2는 pointer object 2개와 int object  도합 3개 object

D2 = D1 + 20;                   // D2는 lvalue, D1은 lvalue conversion 되어 value 10으로 사용됨
E2 = &D2;                         // E2는 lvalue, D2도 & 연산자의 피연산자로 lvalue로 사용됨
*E1 = *E2;                         // *E1 (= D1)은 lvalue, *E2(=D2)는 lvalue conversion 되어 value 30으로 사용됨
**F1 = **F2;                       // 3개 object 중에서 3번째 int object가 **F1은 lvalue, **F2는 value로 사용됨
*F1 = *F2;                         // 3개 object 중에서 2번째 pointer object *F1는 lvalue, *F2은 value로 사용됨. 

 

위의 예에서 E1은 선언되면서 &D1으로 초기화 되었습니다. E1의 type은 int * (pointer to int)이고, *E의 type은 int입니다. 2개의 object가 선언되고, E1은 &D1으로 초기화 되고, *E1(=D1)은 위에서 10으로 각각 초기화 되었습니다. 이 것이 사용될 때에 E1으로 사용되면 첫번째 객체의 lvalue이고, *E1으로 사용되면 두번째 객체(D1)의 lvalue라는 것을 "척 보면 앱~~니다.". 이중 포인터로 선언된 F1과 F2도 마찬가지입니다. 3개의 object가 선언되었고, F1과 F2는 첫번째 local stack에 선언된 pointer objec입니다. **F1, **F2는 세번째 object인 D1과 D2의 lvalue입니다. 어느 position에 사용되느냐에 따라서 lvaue로 사용되거나, lvalue conversion이 일어나서 세번째 object의 value로 사용될 수도 있습니다. *F1과 *F2는 각각 두번째 object인 E1과 E2의 lvalue라는 것을 알수 있습니다. 

 

이상으로 포인터를 공부하기 위한 기초체력 증강 프로젝트를 모두 마치겠습니다. ^^ 지난 이야기와 이번 회차의 이야기는 깊이 있게 파고 들어 완전히 이해하려고 하면 꽤나 어려운 내용일 수 있습니다. 쭈웈 읽으시면서 여기까지 오셨으면 더 뒤돌아 보지 않으셔도 됩니다. 다음 글에서 본격적으로 포인터 이야기 시작해 보겠습니다.  

 

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

 

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

 

[오리뎅이의 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일 수원에서 뒤뚱뒤뚱~~~~ [오리]

 

[오리뎅이의 라우팅 이야기 - 4] Static Routing의 시작은 PC와 서버

오리뎅이의 라우팅 테이블 훔쳐보기

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

 

터기 이스탄불 피에르로티 언덕에서 내려다 본 도시 풍경

위 사진은 2014년에 터키 이스탄불에 출장을 갔을 때, 찍은 사진이에요. 터키 보다폰 통신 사업자 사무실에서 회의를 마치고, 저희 법원 사무실로 돌아가기 위해서 택시를 탔는데요. 택시 기사가 길을 헤매이다가 내려 놓은 곳이 바로 이 언덕 뒷편이었어요. 저희 법원 사무실은 사진 오른쪽 언덕 중간 즈음에 있거든요. ^^. 저는 이전에 사우디에 주재원으로 있을 때, 가족 여행으로 이곳에 와봤던 적이 있어서 택시가 피에르로티 언덕 뒷편 즈음에 와 있는 것을 알겠더라구요. 그래서 마침 같이 출장 갔었던 동료도 있고 해서 좀 둘러 봐야겠다는 심정으로 기사님께 거기에 그냥 내려 달라고 했어요. 느낌으로 언덕 정상만 넘어가면 나올것 같아서 느낌만 믿고 이 피에르로티 언덕 앞쪽으로 걸어 왔어요. 택시 기사님이 헤매신 덕분에 출장 중에 이 아름다운 피에르로티 언덕을 관광 하게 되었던 것이지요. 사진으로 봐도 넘 멋진 풍경이네요. 코로나로 집콕을 너무 오래동안 해서 그런지 정말 너무 너무 다시 가보고 싶으네요. ^^ 

Static Routing 프롤로그

Connected에 이은 두번째 오리뎅이의 라우팅 이야기 주제는 Static Routing입니다. Connected는 PC나 서버, 그리고, 라우터와 같은 IP를 사용하는 장비에서 인터페이스에 IP와 Subnet을 설정하면 라우팅 테이블에 자동으로 Connected 엔트리가 생성 됩니다. 같은 subnet에 있는 다른 장비들과의 통신은 별도로 다른 라우팅 설정을 하지 않더라도 라우팅 테이블에 생성된 Connected로 통신이 됩니다. Static Routing 프로토콜 또는 Dynamic Routing 프로토콜을 이용해서 라우팅 설정을 해야 하는 경우는 라우팅 장비를 통해서 다른 네트워크에 떨어져 있는 장비와 통신을 하고자 하는 경우에 필요합니다. Static Routing은 PC와 서버는 물론이고, 라우터, Firewall, L4-L7 switch 등에서 매우 광범위하게 사용되고 있습니다. Dynamic Routing 프로토콜 이야기를 시작하기에 앞서 Static Routing의 BoB(Basic of Basic)를 하나씩 하나씩 찬찬히 살펴 보겠습니다.

 

    - Linux, Windows 10, 라우터 각각에서 Static Route 설정 방법 

       . 기본 Static Route 설정 방법

       . 4가지 파라미터 사용 방법

       . 설정 영구(Permanent) 적용 방법

    - Redundancy

       . Static Route 장애 감지

       . Link Redundancy vs L2 Device Redundancy vs L3 Device Redundancy 

       . L2 Redundancy

         → Bonding/Teaming

         → LACP

       . FHRP : VRRP, HSRP

       . Floating Static Route

       . ECMP

    - 빠른 장애 감지 및 절체 - BFD

       . Static Route + ECMP + BFD

    - Default Route 설계

    - Router On a Stick

    - Control Plane vs Data(Forwarding) Plane : 평면이 대체 모라구?

       . L2 SW, L3 SW, Router, L4-L7 Switch, Firwall등 각 장비의 CPU는 어디에 있나?

 

해야 할 이야기들이 정말 많아 보이네요. 그래도 하나씩 하나씩 풀어 가다보면, 곧 끝이 보이겠지요. 시작이 반이라고 했으니, 이미 절반은 끝낸 샘입니다요. ^^

 

Static Routing 설정 방법 1, 2, 3

[오리뎅이의 라우팅 이야기 - 3] Connected - IP 네트워킹, 라우팅의 시작 - 3/3 편에서는 Windows 10, Linux, 그리고 Cisco 라우터에서 Static Route를 설정하는 아래의 3가지 설정 방법에 대해서 살짝 맛보기로 알아 보았었습니다.

 

  방안 1) Gateway(PC or 서버) 또는 nexthop(라우터) 만 설정하는 경우 (이후 [gateway|nexthop]로 표시)

  방안 2) Interface만 설정하는 경우

  방안 3) [gateway|nexthop] Interface 둘 모두 설정하는 경우

 

* PC 또는 server와 같은 End host 장비에서는 다른 네트워크와 통신하기 위해서 설정하는 라우팅 디바이스의 IP를
   gateway라고 부르고, router에서는 다른 routing 장비를 통해서 간접 전달을 해야 하는 경우, 설정해야 하는 다른
   라우팅 장비의 IP를 nexthop이라고 부릅니다.

 

각 방안을 사용하는 경우의 특징으로는 다음과 같은 것들이 있었습니다.

 

  방안 1)을 사용하는 경우는 recursive lookup이 발생한다. Interface down 되어도 엔트리가 삭제되지 않는다

  방안 2)를 사용하는 경우는 대상과 직접 통신을 시도한다. 모든 대상 IP로 직접 ARP resolution 시도한다

  방안 3)을 사용하는 경우는 Recursive lookup 발생하지 않고, Interface가 down 되면 엔트리가 자동으로 삭제되고, ARP resolution은 [gateway|nexhop] 1개에 대해서만 생성된다.  

 

방안 3)으로 하는 것이 가장 이상적인 방법이란 것을 딱봐도 쉽게 알 수 있습니다. 특별한 이유가 있어 방안 1)이나 방안 2)를 사용 해야하는 경우가 아니라면, 방안 3) 으로 설정하는 것이 좋습니다.

 

* 방안 1)을 사용해야만 하는 특별한 이유 : 장비가 2가지 모두 설정하는 것을 지원하지 않는데, 방안 2)를 사용할 경우, 너무 많은 ARP table 엔트리가 생성될 수 있는 상황인 경우
* 방안 2)를 사용해야만 하는 특별한 이유 : 서로 다른 subnet 대역에 있는 peer간 통신이 되도록 해야 하는 경우

 

Static Routing의 시작은 - PC or 서버

그림1. PC와 서버간 Static Routing 설정 설명을 위한 네트워크

그림1에는 점선 타원으로 표시한 5개의 서로 다른 LAN(Local Area Network)이 있습니다. 좌측에 있는 PC0과 PC1은 SW0를 통해서 R0에 연결되어 하나의 LAN을 구성합니다. 오른쪽의 Server0과 Server1도 SW1을 통해서 R2에 연결되어 또 다른 하나의 LAN을 구성합니다. 라우터와 라우터 사이는 직접 direct로 연결 되더라도 독립적인 하나의 LAN입니다.  PC와 서버는 서로 다른 네트워크에 있기 때문에 직접 통신이 불가능하고, 라우터를 통해서 간접 통신만 가능합니다. Static Routing 설정을 통해서 PC와 서버가 서로 통신이 되도록  해 보겠습니다.

 

PC와 서버가 서로 통신을 하기 위해서는 PC에서 서버로 가는 길(route), 서버에서 PC로 돌아 오는 길(route)의 양방향 route가 모든 라우팅 홉에 설정이 되어야 합니다. 라우팅 설정의 시작은 양쪽 end host인 PC와 서버에서 시작됩니다. 네트워크를 처음 배우는 분들은 패킷 트레이서나 GNS3와 같은 시뮬레이터로 실습을 할 때, 종종 PC나 서버의 라우팅 설정을 하지 않는 경우가 있습니다. 좀 더 심한 경우는 라우팅 테이블은 라우터에만 있다고 생각하는 경우도 있습니다. 이는 아마도 집이나 회사의 PC를 사용할 때에 라우팅 테이블 설정을 별도로 할 일이 없기때문에 PC에는 라우팅 테이블이 없다고 생각하게 되었을 수도 있을 것 같습니다.  PC에도 라우팅 테이블 있습니다. ^^

 

Windows 10 Static Routing 설정

Windows 10 PC에서의 Static Route 설정은 아래 참조1과 같이 4가지 파라미터들이 사용될 수 있습니다. MASK 까지 별도로 한다면 파라미터가 5가지 이지만, 의미상 Destination과 Mask(또는 Prefix)가 함께 목적지 네트워크를 나타내므로 하나의 파라미터라고 간주하고 이야기 하겠습니다. 

 


Windows 10 Static Route 
설정
   
route add 10.10.13.0 MASK 255.255.255.0  10.10.10.1 METRIC 3 IF 2
             destination^                           ^gateway    metric^    ^interface

route -p add10.10.13.0/24 10.10.10.1 METRIC 3 IF 2    // -p (permanent) 옵션 : 재시동 되더라도 테이블 유지

참조1. Windows 10 Static Routing 설정 예

 

Destination은 MASK (또는 Prefix)와 함께 목적지 네트워크를 나타냅니다. MASK 대신에 10.10.13.0/24 와 같이 Prefix를 이용해서 목적지 네트워크를 나타낼 수도 있습니다.  Destination은 Static Routing 설정에 필수 파라미터입니다. MetricInterface는 필수 파라미터가 아닙니다. 생략할 수 있습니다. 

 

그림2. Windows PC의 Interface와 Gateway

 

Gateway 없이 Interface만 이용해서 설정하는 방법은 Linux와 라우터에서는 가능하지만, Windows 10에서는 설정 방법이 다릅니다. Gateway를 생략하는 대신에 0.0.0.0 으로 설정하거나 Local IP로 설정을 합니다.  특별한 경우가 아니라면면, Gateway Interface 둘 모두를 설정하는 것이 좋습니다.  Windows 10에서 IF 2, IF 9 와 같이 IF 옆의 숫자는 Interface를 나타내는 Index 번호입니다. Interface Index 번호는 windows cmd 창에서 route print 명령을 입력했을 때, 가장 윗부분에 아래 참조2와 같이 이더넷 어답터 별로 가장 앞에 Index 번호가 출력됩니다.

 

1
2
3
4
5
6
7
C:\Users\user>route print
===========================================================================
인터페이스 목록
  9...b8 ac 6f 1d 1e 36 ......Intel(R) 82567LM-3 Gigabit Network Connection
 10...0a 00 27 00 00 0a ......VirtualBox Host-Only Ethernet Adapter
  1...........................Software Loopback Interface 1
===========================================================================
cs

참조2. Windows 10 라우팅 테이블의 IF 번호

 

Windows 10에서 Static Route의 Metric은 자동 매트릭 기능으로 Interface별로 Bandwidth에 따라서 자동으로 할당 되는 Interface Metric과 Routing Metric의 합계 값이 route print 명령의 결과로  routing table 출력에 매트릭 필드에 표시 됩니다.  Interface 별 자동 매트릭 기능은 Interface의 Bandwidth 별로 다음의 그림3과 같이 기본 값이 정해져 있습니다. 

 

그림3. Windows 10 Interface Bandwidth별 Meric

그림3 출처 : https://docs.microsoft.com/en-us/troubleshoot/windows-server/networking/automatic-metric-for-ipv4-routes

 

 

그림3에서 Link Speed는 Tx/Rx Speed를 합산한 값입니다. 따라서 1G Ethenret Interface인 경우 Greater than or equal to 2GB에 해당되어 Meric이 25입니다. Static Routing 설정에 대한 Routing Metric은 값을 지정하지 않으면 기본값이 1입니다. 따라서 기본값을 주지 않고,  1G Ethernet Interface에 static route를 추가한 경우는 참조 3과 같이 Interface Metric 25와 Routing Metic 1을 합산한 값 26이 매트릭으로 표시 됩니다. 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 
C:\WINDOWS\system32>route add 172.11.11.1/24 192.168.35.1 IF 9
 확인!
 
C:\WINDOWS\system32>route print
===========================================================================
인터페이스 목록
  9...b8 ac 6f 1d 1e 36 ......Intel(R) 82567LM-3 Gigabit Network Connection
 10...0a 00 27 00 00 0a ......VirtualBox Host-Only Ethernet Adapter
  1...........................Software Loopback Interface 1
===========================================================================
 
IPv4 경로 테이블
===========================================================================
활성 경로:
네트워크 대상      네트워크 마스크     게이트웨이      인터페이스    메트릭
          0.0.0.0          0.0.0.0     192.168.35.1   192.168.35.120     25
        127.0.0.0        255.0.0.0           연결됨         127.0.0.1    331
        127.0.0.1  255.255.255.255           연결됨         127.0.0.1    331
  127.255.255.255  255.255.255.255           연결됨         127.0.0.1    331
      172.11.11.0    255.255.255.0     192.168.35.1   192.168.35.120     26
 
cs

참조3. Windows 10 매트릭 = Interface Meric + Routing Metric 

 

Windows 10 cmd 창에서 PowerShell을 실행하고, get-netroute 명령을 이용하면, 아래 그림4와 같이 RouteMetric과 ifMetric이 각각 구분되어 표시되는 것을 확인할 수 있습니다. 

 

그림4. PowerShell 에서 get-netroute 로 라우팅 테이블 확인하기

 

PC가 재시동 되더라도 Static Routing으로 추가한 엔트리가 유지되도록 하려면 참조1의 아래 부분 예와 같이 -p 옵션을 사용하면 됩니다. 

 

 

Linux 서버 Static Routing 설정

Linux는 Ubuntu, Debian, Centos, Fedora, RHEL 등 많이 버전들이 있습니다. 어느 버전에서나 "ip route add" 명령과 "route add" 명령을 모두 사용해서 Static Route 설정을 할 수 있습니다. 모든 최신 리눅스 배포판에서는 "ip route add" 명령을 사용할 것을 권고합니다.

 


Linux Static Route 설정
예) ip route add 10.10.10.0/24 via 10.10.13.1 dev eth0  metric 1
                      destination^      ^gateway          ^interface ^metric

예) route add 10.10.10.0/24 gw 10.10.13.1 dev eth0 metric 1

참조4. Linux Static Route 설정 예

 

Windows 10에서와 마찬가지로 Destination은 Static Routing 설정에 필수 파라미터입니다.  gateway와 interface는 둘중에 하나만 사용할 수 있습니다. 둘 다 생략할 수는 없습니다. 

 

그림5. Linux server의 Interface와 Gateway

 

Linux의 경우에도 특별한 경우가 아니라면, Gateway와 Interface 둘 모두를 설정하는 것이 좋습니다. Gateway만 설정하고 Interface를 설정하지 않으면, recursive lookup을 통해서 적절한 Interface를 찾습니다. 반대로 Interface만 설정하고, Gateway를 설정하지 않는 경우는 모든 목적지 IP에 대해서 직접 통신이 가능한 것으로 판단하고 직접 ARP Resolution을 시도할 것입니다. 같은 LAN에 목적지 장비가 있을 경우에는 통신에 문제가 없지만, 다른 network에 존재하는 목적지인 경우에는 라우터에 Proxy ARP 기능이 Enable 되어 있는 경우에만 라우터 인터페이스의 MAC address로 ARP Resolution이 됩니다. 목적지가 많아지면, ARP table entry가 늘어나서 ARP table lookup에 시간이 오래 소요되거나 table이 부족한 상황이 초래될 수도 있습니다. 그러니, 특별한 사유가 없는 한 Gateway Interface 둘 모두를 설정하는 것이 좋습니다.

 

Static Route 설정 시 Metric을 설정하지 않으면, 기본 값으로 1로 설정됩니다. Windows 10과는 달리 Interface Metric과 Route Metric이 별도로 있지 않고 그냥 Metric 만 있습니다.  동일 네트워크에 대해서 Metric이 낮은 엔트리가 우선하므로 Metric이 0인 Connected가 우선 순위가 가장 높고, Metric이 1인 Static Routing은 그 다음으로 우선 순위가 높습니다.  

 

Comand Line Interface 명령어로 추가한 Route 엔트리(Entry)는 서버가 재시동 되면 사라집니다. Static Route  설정을 서버가 재시동 되어도 라우팅 테이블에 그대로 유지되도록 하는 것은 각 Linux 배포판 별로 조금씩 상이합니다.

Ubuntu는 Version 17.10 부터는 /etc/netplan/ 디렉토리에 있는  yaml 화일에 static route를 아래 참조 4와 같이 추가합니다. Interface별로 default gateway는 gateway4: 에 gateway IP를 적고, static route의 경우는 routes: 에 to: Destination/Prefix, via: Gateway IP, metric: metric value  를 적습니다. metric은 생략할 수 있습니다. 생략할 경우 default value로 0 이 설정됩니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# This is the network config written by 'subiquity' 
network: 
  version: 2 
  ethernets: 
    eno0: 
      addresses: 
      - 10.10.10.2/24 
      gateway4: 10.10.10.1      // default gateway
        addresses: [8.8.8.8]
    eno1: 
      addresses: 
      - 172.16.21.6/24 
      routes: 
      - to: 172.16.23.0/24      // static route
        via: 172.16.21.1 
        metric: 100
cs

참조5. Ubuntu /etc/netplan/01-netcfg.yaml 화일에 static route를 추가

 

참조4와 같이 파일에 추가 한 후에 "sudo netplan ap+ply" 명령으로 적용합니다. 적용이 잘 되었는지 "ip route show" 또는 "route -rn" 명령으로 확인합니다. 적용이 잘 되었다면, 어떠한 이유로 서버가 재시동 되더라도 추가된 Route 엔트리는 계속 유지 됩니다.

 

RHEL 및 Centos 7.x 버전에서는 /etc/sysconfig/network-scripts/ 디렉토리에 있는 interface별 화일에 default gateway 및 static route를 추가합니다. Interface eno0에 default route와 static route 를 설정하는 경우는 참조5와 같이  ifcfg-eno0 화일에 설정 합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@DUCK]# cat ifcfg-eno0
TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=none
DEFROUTE=yes                 // 이 인터페이스를 default gateway로 설정
IPV4_FAILURE_FATAL=no
NAME=eno0
DEVICE=eno0
ONBOOT=yes
IPADDR=10.10.10.3           // static route 목적지 network or host IP 
PREFIX=24                   // static route 목적지 네트워크 prefix length
GATEWAY=10.10.10.1          // static route Gateway IP
DNS1=8.8.8.8
cs

참조6. RHEL 및 Centos 7.x 버전에서 static route를 영구 설정하는 방법

 

Default route를 영구 저장하기 위해서 참조4(Ubuntu - gateway4)나 참조5(RHEL - DEFROUT)와 같이 설정을 하는데요. Linux 시스템이 multiple interface를 가지는 경우, default gateway를 모든 interface에 copy 해서 적용하는 경우가 종종 있습니다. 그럴 경우 의도치 않은 인터페이스로 패킷이 라우팅 되어 인터넷 접속이 안되거나 원하는 목적지로 패킷이 라우팅 되지 않아 접속이 안되는 사태가 벌어집니다. ECMP (Equal cost Multiple Path)를 적용하기 위해서 복수의 Interface에 default gateway를 설정하는 경우가 아니라면 default gateway는 1개의 interface에만 추가해야 합니다.  Redudnancy 및 link 용량 증설을 위해서 ECMP를 사용하는 방법에 대해서는 추후 글에서 자세히 알아 보겠습니다.

Router의 Static Route 설정 방법

PC나 서버 같은 end host 장비에서 다른 네트워크로 보내기 위해서 패킷을 전달해야 할 라우팅 장비의 IP를 gateway라고 부릅니다. 반면에 라우팅 장비에서는 목적지로 보내기 위해서 패킷을 내 보내야 할 다른 라우팅 장비의 IP를 nexthop이라고 부릅니다. Host 장비(PC or 서버 등), 라우팅 장비 불문하고, 자기 자신의 인터페이스에 직접 연결된 상대방으로 보내는 경우는 Connected로 보내면 되기 때문에 라우팅 테이블에 gatewaynexthop이 필요하지 않습니다. 오직 자기 자신의 Interface에 속하지 않은 network로 패킷을 간접 전달해야 하는 경우에 gateway 또는 nexthop이 필요합니다.

 

 

그림6. PC에서 Server로 가는 길 설정

 

Static Routing 설정 명령어와 방법은 각 네트워크 장비 벤더별로 조금씩 다릅니다. 

 

Cisco Router에서는 static route 설정 시 interface와 nexthop 둘 중에 하나만 사용하거나 둘 모두를 사용할 수 있습니다. Linux 경우와 마찬가지로 interface만을 사용할 경우에는 모든 목적지에 프레임을 직접 전달할 하기 위해서 ARP resolution을 시도합니다. 반면에 nexthop만을 사용할 경우에는 출구 interface를 찾기 위해서  라우팅 테이블 recursive lookup이 발생 합니다. 

 

특별한 사유가 없는 한 interface nexthop 둘 모두를 사용하는 것이 좋습니다. 

 

                         
Cisco Router Static Route 설정

예) ip route 10.10.13.0 255.255.255.0 10.10.11.2  25                 // Old IOS 방식은 nexthop 또는 interface 중 1개만 가능
        destination^               nexthop^           ^AD

    ip route 10.10.13.0 255.255.255.0 Gi0/1  25                        // Old IOS 방식은 nexthop 또는 interface 중 1개만 가능
        destination^             interface^      ^AD                           
    ip route 10.10.13.0 255.255.255.0 Gi0/1 10.10.11.2 25          // New IOS 방식은 nexthop, interface 동시 사용 가능  
        destination^             interface^  nexthop^     ^AD 

참조 7. Cisco Router Static Route 설정 예

 

PC와 Linux에서는 GatewayInterface 이외에 Metric 값을 옵션으로 설정할 수 있었습니다. 그런데, Cisco Router에서는 Metric 값은 설정할 수 없습니다. Static Route의 Metric 값이 설정은 안되지만, 라우팅 테이블 출력 시에는 그림7과 같이 Metric 값이 0으로 표시는 됩니다.

 

그림7. Cisco Static Route [1/0] = [AD/Metric]

그러나 Cisco 장비에서 Static Route의 Metric 0은 의미가 없는 dummy 값입니다. Metric 값은 설정할 수 없지만, Cisco Router에서는 옵션으로 AD(Administrative Distance) 값을 설정할 수 있습니다. AD 값은 라우팅 프로토콜들 별로 우선 순위에 차등을 주는 값입니다. Cisco에서는 AD라고 부르고, 주니퍼에서는 Preference라고 부릅니다. 프로토콜 별로 기본값들도 다릅니다. 어떤 값들을 사용하는지는 구글에서 찾아 보면 그림 8과 같은 잘 정리해 놓은 그림들이 뚝딱 나옵니다. ^^

 

 

그림8. AD or Preference

그림8 출처 : https://www.routexp.com/2020/03/check-out-value-administrative-distance.html

 

 

Cisco만 AD라고 부르고 나머지 벤더에서는 Preference 라고 부르는 군요. Cisco, Juniper, HP는 모두 Static Route의 우선 순위가 Connected 다음으로 높은 우선 순위를 보여 주고 있는데요. Huawei 는 특이하군요. Huawei는 안쓸거니까 그러거나 말거나 패쑤!! ^^

 

그림9. Server에서 PC로 돌아 오는 길 설정

 

주니퍼 라우터의 Static Route 설정은 시스코와 다른 점이 몇가지 보입니다. 우선 위의 그림8과 같이 Preference라는 값으로 각 프로토콜들이 생성한 route의 우선 순위를 부여합니다. 그리고 동일 프로토콜이 생성한 route에 대해서 주니퍼 라우터에서는 metric 값으로 우선 순위를 부여할 수 있습니다.

 

                         
Juniper Router Static Route 설정

예)  set routing-options static route 10.10.13.0/24 next-hop 10.10.11.2  preference 30 metric 15            // nexthop 
                                            destination^            nexthop^          prefernece^
  metric^            
               
    set routing-options static route 10.10.13.0/24 next-hop Gi0/1                      //  interface 가능  
                                         destination^          interface^

참조8. Juniper Router Static Route 설정 예

 

주니퍼 라우터에서는 next-hop 뒤에 기본적으로  nexthop IP만 사용합니다. 그리고 기본적으로 nexthop IP는 directly connected IP만 사용합니다. 즉, 직접 연결된 라우터의 IP만 nexthop IP로 사용할 수 있습니다. 지금까지 설명으로 사용한 예에서 nexthop은 모두 direct conencted 인 IP만 사용했었는데요. 밑에서 자세히 설명 드릴 내용이지만 nexthop IP를 direct connected IP가 아닌 원격의 라우터 인터페이스 IP를 사용할 수도 있습니다.  주니퍼 라우터에서는 이렇게 direct connected IP를 nexthop으로 사용하는 경우에는 별도로 interface를 옵션으로 추가해 주지 않더라도 그림9와 같이 라우팅 테이블에 interface가 자동으로 추가되어 패킷 라우팅 시에 recursive lookup이 일어 나지 않습니다. 이것은 매우 바람직한 기능 같습니다.

 

그림10. 주니퍼 라우터는 Interface를 지정하지 않아도 자동빵 추가

 

PC, 서버, 시스코 라우터에서 Static Route 설정 시에 Interface만 줄 수 있었던 것처럼 주니퍼 라우터에서도 next-hop 뒤에 IP가 아닌 Interface를 줄 수 있습니다.  Interface만 주는 경우는 역시나 모든 목적지를 직접 연결된 것으로 간주하여 모든 IP에 대해서 직접 ARP Resolution을 시도합니다.

 

Cisco Route에서는 metric을 옵션으로 지정할 수 없었으나 주니퍼 라우터에서는 metric도 설정할 수 있습니다.  Metric을 생략할 경우 기본 값으로 그림8과 같이 5가 사용됩니다.

 

원격 [gateway|nexthop] 지정하기

앞에서 언급했던 바와 같이 지금까지의 Static Route 설정 예에서는 gateway 혹은 nexthop을 direct connected 상태인 인접 라우터의 interface IP를 사용했습니다. gateway|nexthop 으로 다른 네트워크에 있는 IP를 사용할 수 있습니다. 단, gateway|nexthop으로 사용 될 원격지 IP로 가기 위해서 어느 Interface를 통해서 나가야 하는지가 라우팅 테이블에 등록이 되어 있어야만 원격지 IP를 gateway|nexthop 으로 사용할 수 있습니다. 즉, Recursive lookup으로 원격지 nexthop IP로 연결되는 출구 interface를 찾을 수 있어야 합니다.

 

Windows 10 원격 gateway 지정하기 - 가능

그림11. Window 10에서도 원격지 IP를 Gateway로 설정할 수 있답니다

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C:\WINDOWS\system32>route add 10.10.13.0/24 10.10.12.2 IF 9
 확인!
 
C:\WINDOWS\system32>route print
===========================================================================
인터페이스 목록
  9...b8 ac 6f 1d 1e 36 ......Intel(R) 82567LM-3 Gigabit Network Connection
 10...0a 00 27 00 00 0a ......VirtualBox Host-Only Ethernet Adapter
  1...........................Software Loopback Interface 1
===========================================================================
 
IPv4 경로 테이블
===========================================================================
활성 경로:
네트워크 대상      네트워크 마스크     게이트웨이      인터페이스    메트릭
          0.0.0.0          0.0.0.0     192.168.35.1   192.168.35.120     25
       10.10.13.0    255.255.255.0       10.10.12.2   192.168.35.120     26
cs

참조9. Windows 10에서 원격지 IP를 Gateway로 설정하기

 

 

참조9를 보면 Windows 10에서 IP 10.10.12.2를 Gateway로 하여 Static Route 설정이 성공적으로 라우팅 테이블에 엔트리가 생성된 것을 볼 수 있습니다. 이는 recursive lookup에 의해서 10.10.12.2는 바로 위에 있는 default gateway를 통해서 도달 가능한 것이 확인 되기때문에 성공적으로 라우팅 엔트리가 추가 된 것입니다.

 

Linux 원격 gateway 지정하기 - 불가능(?)

Linux는 Static Route 설정 시 indrect nexthop IP를 Gateway로 허용하지 않는 것으로 보입니다. 지금까지 찾아 본 바로는 원격지 IP를 gateway로 주어서 설정을 성공시키지 못했습니다. 구글링으로도 방법을 찾지 못했습니다. 지원하지 않는다는 내용도 찾지를 못했기때문에 미확인입니다. 심적으로 판단하기에는 지원하지 않는 것 같습니다.

 

그림12. Router에서 Static Route의 nexthop을 원격지 IP로 설정하기 

 

Cisco Router 원격 nexthop 지정하기

Cisco Router는 원격지 IP를 nexthop으로 설정하는 것이 가능합니다. Interface 없이 nexthop만 설정한 경우에 recursive lookup을 통해서 출구 interface를 찾는다고 했던것처럼 원격지 IP를 nexthop으로 설정한 경우에도 해당 원격지 IP에 도달 가능한 route가 라우팅 테이블에 있다면 정상적으로 recursive lookup으로 출구 interface를 찾아서 패킷을 forwarding 합니다.  만일 recursive lookup으로 출구 interface를 찾을 수 없는 경우에는 라우팅 테이블에 엔트리가 올라오지 않기 때문에 라우팅 lookup 실패로 패킷을 라우팅 하지 못하고 드롭합니다. 아래 그림13을 보면, 1에서 원격지 IP를 nexthop으로 static route 설정을 하지만, 2에서 확인 시 라우팅 테이블에 엔트리가 올라오지 않습니다. 3에서 1에서 설정한 nexthop IP에 대한 static route를 추가하니 4에서 원격지 IP를 nexthop으로 설정한 Static Routing 엔트리도 라우팅 테이블에 올라 온 것이 보입니다.

 

그림13. Cisco Router에서 원격지 IP를 nexthop으로 설정하기

"Recursive lookup으로 출구 interface를 찾는다"는 의미는 원격지 IP를 nexthop으로 설정한 경우에도 Ethernet Frame은 직접 연결된 인접 라우터를 통해서 간접 전달이 되어야 하므로, directly connected 관계인 인접 라우터의 IP와 해당 네트워크 Interface가 recursive lookup에 의해서 검색되어야 한다는 의미입니다. 그림12의 R0 라우터에서 10.10.13.0/24 네트워크에 대한 nexthop을 직접 연결된 라우터 R1이 아니라, R1을 통해서 연결된 원격지 라우터 R2의 10.10.12.2를 nexthop으로 설정하는 경우에 recursive lookup에 의해서 10.10.12.2/24 네트워크에 대한 nexthop이 그림12의 4와 같이 직접 연결된 인접 라우터 R1의 10.10.11.2가 nexthop으로 검색 되어야 합니다.   

 

Juniper Router 원격 nexthop 지정하기 - resolve

Juniper Router에서도 원격지 IP를 nexthop으로 설정할 수 있습니다. Cisco Router나 Windows 10에서는 별도의 설정을 하지 않더라도 recursive lookup으로 라우팅 테이블에서 출구 interface를 찾습니다. 그리고, recursive lookup에 의해서 출구 interface가 찾아지면 패킷을 해당 출구 interface로 forwarding 하고, 출구 interface를 찾지 못하는 경우에는 패킷을 드롭 합니다.  Juniper Router는 이와는 다르게 원격지 IP를 nexthop IP로 설정하는 경우에는 resolve 옵션을 함께 사용해야지만, recursive lookup을 수행합니다.

 

그림14. Juniper Router는 resove 옵션을 명시적으로 사용해야 원격지 IP nexthop IP recursive lookup 수행

 

그림14의 1번에서 Destination network 10.10.13.0/24 에 대한 next-hop IP를 원격지 IP인 10.10.12.2로 설정하고, 2번에서 10.10.12.0/24 network에 대한 static route까지 추가를 하더라도 라우팅 테이블에는 10.10.13.0/24에 대한 엔트리가 올라오지 않습니다. 4에서 10.10.13.0/24 에 대해 resolve 옵션을 추가로 설정하자마자 5번과 같이 10.10.13.0/24 network에 대한 엔트리가 라우팅 테이블에 생성되었습니다. Nexthop과 Interface가 recursive lookup을 통해서 인접 라우터의 nexthop IP인 10.10.11.2로 되어 있는 것을 볼 수 있습니다.

 

 

Windows 10, Linux, 라우터 등에서 Static Routing의 BoB에 대해서 알아 보았습니다. 오늘 알아본 BoB 내용을 바탕으로 다음 시간에는 Straic Routing의 장애 감지, Redudancy, High Availability, ECMP 등의 한 단계 차원 높은 주제들에 대해 알아 보겠습니다.

 

좀 이상하다 싶은 내용이 있으시면, 가차없이 태클을 날려 주세요. 오타를 보시면 콕콕 찝어 주세요. 추가적으로 궁금하신 내용이 있으시면 댓글에 질문을 달아 주세요. 태클과 질문을 통해서 쑥쑥 자라는 블로그가 되기를 희망해 봅니다.

 

2021년에 새로운 글로 찾아 뵙겠습니다. 구독해 주신 여러분!!! 새해 복 많이 받으세요!! 

 

2020년 12월 31일에 수원에서 뒤뚱뒤뚱 ~~~~ [오리]