C 마이크로프로세서 프로그래밍/포인터

위키책, 위키책


포인터 변수 모두는 메모리의 주소를 지정하는 값을 가지고 있으면 값을 변화시킬 수 있기 때문에 CPU을 설계한 설계 기준에 따라 주소값의 길이와 방식이 결정된다. 일반적인 용도의 대부분의 CPU는 메모리를 지정하는 길이(비트수)는 동일하다. RAM이든 ROM/FLASH 이든 모든 주소는 같다. MCU(8051,...)은 오히려 많은 경우 메모리 영역을 나누어 다른 주소체계를 사용 한다. 8051은 내부의 256바이트 내에 변수를 할당 한다. 256바이트는 매우 적기 때문에 많은 데이터 저장용으로 16비트의 저장 공간을 갖는 주소체계를 사용하고 기계어 코드를 분리 했다. 이럴 경우는 주소값이 8비트 또는 16비트가 필요하다.

C언어가 UNIX 계열의 OS 작성 할 때 사용하였으므로 커널의 프로그램 소스를 보면 상당히 많은 부분 포인터 변수를 볼 수 있다.

포인터 변수의 선언[+/-]

포인터 변수는 *을 이용하여 선언 한다. 포인터는 메모리의 주소값을 가지고 데이터의 위치를 지정하기 때문에 다른 변수의 저장 공간의 주소를 알아야 한다. 따라서 정적 변수의 주소는 & 연산자를 사용 한다.

  int ival;
  int *pval;

  // ...

  pval = &ival; // ival의 &연산자는 ival가 존재하는 위치 주소값이다.  
  *pval = 30;   // pval가 ival의 주소값을 가지고 있으므로, 
                // 이 주소값을 먼저 읽어 30을 쓸 위치를 결정하고 
                // 그 위치에 정수 값을 쓴다.
  printf("변수 pval가 존재하는 위치 주소값은 0x%08X, 정수형 데이터 저장공간 지정은 0x%08X\n", &pval, pval );

ival는 정수형 데이터가 들어가는 변수이다. 즉, 정수 숫자를 저장 한다. 그러나 변수 pval은 데이터 공간의 위치를 지정하는 것이지 정수형 데이터를 저장하는 것은 아니다. CPU의 주소체계에서 메모리의 주소값을 저장 함으로써 데이터가 저장 될 주소값을 가지고 액세스 하는 것이다. 그러나 메모리 주소값도 하나의 이진수의 정수형의 일종이라고 생각 할 수 있다.

NULL 사용[+/-]

포인터 변수는 다른 정적 변수나 동적변수(malloc(), new)에 의해 존재하는 저장 공간을 지정하는 변수이다. 그러나경우에 데이터 저장공간을 지정하지 못하고 액세스 할 수 있다. 보통 저장공간 지정은 했는지 하지 않았는지는 NULL을 이용할 수 있다.

NULL 사용 예:

  char data8;
  char *pval = NULL;  // 포인터가 데이터 저장공간을 지정하지 않았다.

   if (pval == NULL)
       pval = data8; // 변수 pval가 데이터 저장 공간을 변수 data8로 지정 한다.
   *pval = 10; // 위에서 지정한 data8에 10을 쓴다.

C/C++에서 NULL은 숫자 0으로 정의 되어 있다. 자료구조 등에서도 '없다'는 의미로 null을 사용하는데, 경우에 따라 정해진 비트수 만큼 이진수로 모두 1인 경우가 있다. 그러나 C/C++에서는 0으로 정의 되어 있으므로 메모리의 0번지에는 특수하게 사용하지 않는다.

만약 포인터 변수가 데이터 저장공간을 지정하지 않았다면 NULL을 사용하여 초기화 시키는 방식이 일반적이다.

포인터 변수형과 액세스[+/-]

포인터 변수의 길이(변수의 비트수)는 마이크로프로세서에 의존 한다. 즉, 마이크로프로세서가 주소 공간과 액세스 체계를 이미 가지고 있기 때문에 C/C++언어의 컴파일러는 이를 따를 뿐이다. 즉, 포인터 변수의 길이는 CPU 의존적으로 결정 되어 있으므로 모든 포인터의 길이는 같다. 그렇다면 왜 포인터 변수 선언 시, 앞에 변수형이 필요한가는 액세스 할 때 데이터 액세스 길이(비트수)를 결정하기 위해서 이다.

32비트 CPU의 대부분은 32비트 주소공간과 32비트 데이터 액세스 단위를 가지므로 다음 예는 32비트라고 가정 하면:

  // 다음 설명에서 나오는 비트 단위는 32비트 CPU의 경우 

  char buff[1024];

  int *pdata = (int*) buff; // 형이 다르나 값을 액세스 할 때, 32비트 정수형으로 액세스 하기 위해 지정
  *pdata = 23;          // 32비트 정수형 쓰기, 따라서 23은 32비트 정수형 이다.
  *((char*)pdata) = -1; // 그러나 char 8비트 정수형으로 변환 하였으므로 -1은 8비트 정수형이다.
                        // 8비트 정수 -1 : 11111111b
  void *pval;      // 데이터 액세스 타입(액세스 비트 단위)가 없는 변수가능 하다. 
                   // 모든 주소값은 32비트 이기 때문이다.
  pval= (void *) buff;  // 형이 맞지 않으므로 형 변경
  *pval = 10;       // '''error : 액세스 단위를 결정할 수 없다. 따라서 기계어 코드를 확정할 수 없다.
  *(char*)pval = 10; // 액세스 단위가 8비트 이므로, 10은 8비트 정수형이며 이 값이 써진다.

코드 중 *pval = 10;은 pval가 void형이므로 액세스 시, 10을 어떻게 규정할지 결정할 수가 없다. 보통 10은 정수형으로 취급 되지만 같은 정수형도 8,16,32비트로 다르다. 따라서 pval의 변수형에 따라서 비트수가 결정되는데 여기서는 void이므로 결정 불가능 하다.

[+/-]

#include <stdio.h>
#include <string.h>
 
/// Global Variables
 
char name[124];
char tel[] = "010-2345-6789";
 
// func.
char *read_name(char *pstr, int szmax);
 
int main(int argc, char**argv)
{
   char *pstr;
 
    pstr = read_name(name, 120);
    printf("이 름 = %s\n", pstr );
    printf("전화 번호 = %s\n", tel);
 
    return 0;
}
 
char *read_name(char *pstr, int szmax)
{
   size_t leng;
   static char bstr[256];
 
    gets(bstr);
    leng = strlen(bstr);
    if (leng >= (size_t)szmax) 
        leng = szmax -1;
    strncpy (pstr,bstr,leng);
    *(pstr+leng) = 0;
 
    return pstr;
}

이 프로그램 예에서 실제 스트링 메모리 공간을 갖는 것은 name, tel, bstr 변수 들이다. 그러나 pstr변수는 스트링 데이터가 들어갈 변수가 아니고 데이터가 들어가야할 위치를 지정하는 변수이다. CPU의 주소체계에 의한 주소값으로 데이터의 위치를 지정하는 것이다. 이 포인터 변수는 메모리 주소값만 가지면 되므로 정해진 길이의 비트수를 가지고 동작 한다.

포인터 변수에서 주소값의 연산[+/-]

포인터 변수가 갖는 메모리의 주소값은 결국 정수형의 2진수 숫자 일 뿐이다. 따라서 CPU내의 ALU을 통해 연산 된다.

일반적인 32비트 CPU의 주소체계는 주로 32비트 메모리 주소값을 사용 한다. 따라서 CPU의 주소값을 정수형으로 보고 값의 치환, 비교, 연산 등이 가능하다.

#define SZ_DATA 100

int main()
{
  int ival[SZ_DATA];
  int *pval = NULL;

  pval = &ival; // ival의 &연산자는 ival가 존재하는 위치 주소값이다.  
  for (int cnt = 0;cnt <  SZ_DATA;cnt++) {
      *pval = 0;
      pval++;
  }
  // ...
}

pval++에서 연산자 ++는 포인터의 주소값을 다음 위치로 하나 더 옮기라는 뜻이다. 보통의 정수형 변수라면 1을 더하는 것이겠지만 이런경우 주소값을 한 칸 더 옮기는 경우이다. 따라서 단순히 1을 더하는 것이 아니고 포인터 변수가 갖는 int형의 크기만큼 더해진다.

pval++에서 ++ 연산자 :

  • ++ : 현재의 주소값 + sizeof(int) => 주소값 변경

예를 들어 현재 &ival[0] == pval == 0x00301200 이라면 :

  0x00301200 + sizeof(int) :  0x00301200 + 4 => 0x00301204 ==> pval == &ival[1]

즉, ival의 인덱스가 0에서 1로 옮겨 간다.

#include <stdio.h>

#define SZ_DATA 100

char gname[SZ_DATA];
char gbuff[SZ_DATA];

int main()
{
  char *psstr = gname;
  char *pdstr = gbuff;
  gets(gname);
  while (*psstr && *psstr != ' ')
      *pdstr++ = *psstr++;
  *pdstr = 0;
  printf("%s\n", gbuff);
  return 0;
}

psstr++ 경우, 예를 들어 현재 psstr == 0x00301200 이라면 :

  0x00301200 + sizeof(char) :  0x00301200 + 1 => 0x00301201 ==> psstr

이 때는 char 변수가 한 바이트 단위로 배열 되기 때문에 주소값이 1씩 ALU에 의해 더해 진다.

typedef struct {
   char *name;
   int  age;
} Man, *PMan;

Man man[] = {
  { "홍길동", 23 },
  { "Kim",    20 },
  { "Song",   19 },
};

#define SZ_MAN sizeof(man)/sizeof(man[0])

int main()
{
   PMan pman = man;
   printf("sizeof(Man) = %d, man=0x%08X\n", sizeof(Man), man );
   for (int cnt = 0;cnt < SZ_MAN;cnt++) {
      printf("0x%08X : %s, %d\n", pman+cnt, (pman+cnt)->name, (pman+cnt)->age);
   }
   return 0;
}

실행결과 :

sizeof(Man) = 8, man=0x00E070A8
0x00E070A8 : 홍길동, 23
0x00E070B0 : Kim, 20
0x00E070B8 : Song, 19

이 경우의 pman+cnt에서

cnt = 1일 때, 포인터의 값을 다음과 같이 계산할 수 있다:

   0x00E070A8 + sizeof(Man)*cnt :  0x00E070A8 + 8*1 => 0x00E070B0 ==> &man[1]

포인터 변수의 길이[+/-]

포인터 변수는 결국 메모리의 변수 위치의 주소값을 다루는 변수이다.따라서 CPU에 따라 길이가 결정된다. CPU에 메모리 주소체계가 컴파일러 보다 우선 설계되기 때문에 해당 CPU에 맞추어 포인터 컴파일러 설계를 한다. 주로 8비트 CPU의 16비트의 주소값을 갖는다. 그러나 8비트 중에 MCU 계열은 주소를 지정하기 위한 비트가 다양하다. 8비트와 16비트가 혼재하기도 한다. 같은 CPU라도 8비트와 16비트를 같이 사용한다는 이야기 이다.

Z-80 16비트 주소체계
8051 8비트와 16비트 혼용


8051의 Keil Complier 예 [1]
#define LEDPORT0  *(unsigned char xdata*) 0xc0000  // 외부 메모리 주소 0xc0000번지

xdata char gGrapLedData[1024]; // 외부 메모리 설정 - 16비트 주소 공간, 
                               // 8051의 내부 RAM이 256바이트 이므로 큰 데이터 처리 불가
xdata char *pxled;   // sizeof(pxled) = 2 : 외부 메모리 설정 - 16비트 주소 공간

char g_ledData[4];   // 내부 메모리 설정 - 8비트 주소 공간
char *pledfont;      // sizeof(pledfont)=1 : 내부 메모리 사용 - 8비트 주소 공간

void main()
{
    // CPU 초기화

    while (1) {
       // ...
       LEDPORT0 = *pledfont++;  
    }
}

CPU의 프로그램에서 CPU의 메모리 체계을 이해 해야 만 포인터 변수의 길이를 알 수 있다.


이에 비해 32비트는 주로 32비트의 주소 비트 수를 갖는다. 거의 모든 CPU가 32비트 이므로 오히려 8비트 CPU 보다 길이가 통일 되어 있다. 64비트로 가면 또 다른 비트 수를 갖을 것이다.

x86(IA-32) 32비트 주소체계
68000계열 32비트 주소체계
ARM 32비트 주소체계

각각의 변수가 들어갈 메모리의 위치와 성격에 따라 메모리를 지정하는 주소값의 비트수는 CPU에 의해 결정 되어 있으므로 어떤 컴파일이든 정해져 있다.

변수의 길이를 알아보는 방법의 예 :

#include <stdio.h>

 struct Man {
    char name[40];
    int age;
 } ;

 struct Man man;

int main(int argc, char**argv)
{
     int leng;

     printf("sizeof(int)=%d\n", sizeof(int) );
     printf("sizeof(int*)=%d\n", sizeof(int*) );
     printf("sizeof(struct Man)=%d\n", sizeof(struct Man) );
     printf("sizeof(struct Man *)=%d\n", sizeof(struct Man *) );

     // ...
     return 0;
}

일반적인 포인터 변수의 길이는

CPU 주소체계 포인트 포인터 길이
8비트 CPU 8080, Z-80 16비트 2 sizeof(int*) = sizeof(char*) = sizeof(void*)=...= 2
8비트 MCU 8051, SAM8 8/16비트 혼용 혼용 메모리 공간에 따라 1 또는 2바이트 - 컴파일러에 공간 할당 옵션 검토.
32비트 CPU x86(IA-32), 68000, ARM 32비트 4 sizeof(int*) = sizeof(char*) = sizeof(void*)=...= 4
  • 32비트이 CPU에서 배열의 포인트를 예[1]


int a[10];
int *pa;

void printvar()
{
   printf("&a[0] = 0x%08X\n", &a[0]);
   printf("&pa = 0x%08X\n", &pa);
   printf("pa = 0x%08X\n", pa);
   printf("fun= 0x%08X\n", (int) fun1);
   return;
}

void fun(int cnt)
{
   pa = a;

   while (cnt) {
      *pa = cnt;
       pa++;
       cnt--;
   }
}

x86의 실행결과 값 예

 &a[0] = 0x0040DF04
 &pa = 0x0040DF00
 pa = 0x0040DF2C
 fun= 0x00401060

각주[+/-]

  1. 1.0 1.1 C언어 포인터 개념 I - 포인터 이해, 포인터 기초 & CPU 구조