썬 스튜디오 팀
 
32- 비트 애플리케이션을 64-비트 애플리케이션으로 변환하는데 발생하는 문제의 근본적 원인은 long 과 포인터의 타입에 따른 int 의 사이즈 변화에 기인합니다. 32-비트 프로그램을 64-비트 프로그램으로 변환할때는 오직 long 타입과 포인터 타입만 32비트에서 64비트로 사이즈가 변화되며, 정수 타입의 int 32 비트 사이즈는 변하지 않습니다. 이는 포인터나 long 타입을 int 타입으로 지정할 때의 데이터 절단시에 문제 발생의 원인이 됩니다. 또한 int 사이즈보다 작은 타입들의 식(expression)을 unsigned long 혹은 포인터로 지정할때 부호 확장에 대한 문제가 발생할 수도 있습니다. 이 글은 이러한 오류들을 회피하는 방법에 대해 다룹니다.
 

64-비트 와 64-비트 데이타 모델의 차이점

32-비트와 64-비트 컴파일 환경의 가장 큰 차이점은 데이타-타입 모델의 변화 입니다. 32-비트 애플리케이션의 C 데이타-타입 모델은 ILP32 모델입니다, 그러므로 intlong 타입 그리고 포인터들이 32-비트 데이타 타입이므로 그렇게 이름지어졌습니다. 64-비트 애플리케이션의 데이타 모델은 LP64 데이타 모델입니다, 그러므로 long 과 포인터 타입이 64 비트로 커졌습니다. C의 정수 타입과 부동소숫점 타입은 두 데이타 모델에서 동일합니다.

현재의 32-비트 애플리케이션에서 int 타입과, long 타입, 그리고 포인터가 같은 크기를 갖는 것은 이상한 일이 아닙니다. 그러므로 LP64 데이타 모델에서 longpointer 의 변화는 이러한 변화 자체가 기본적인 ILP32-to-LP64 변환 문제의 원인이 됩니다.

lint 유틸리티를 사용하여 64-비트 long 과 포인터 타입 문제점 찾기

lint를 사용하여 32-비트 와 64-비트 컴파일 환경 둘다를 위해 쓰여진 코드를 체크합니다. -errchk=longptr64 옵션을 명시하여 LP64 주의(warning) 를 생성하도록 합니다. 또한 -errchk=longptr64 플래그를 사용해서 long interger와 포인터가 64비트인 환경에서의 포팅가능성을 체크하고 일반 정수가 32비트 사이즈를 가지고 있는지 체크 합니다. -errchk=longptr64 플래그는 포인터 표현의 할당을 체크하고 명시적인 캐스트가 사용되었더라도 long integer 표현의 일반 정수형 할당 또한 체크합니다.

-errchk=longptr64,signext 옵션을 사용하여 정상적인 ISO C의 값-보존 규칙이 unsigned 정수형 타입들의 표현을 통해 signed 정수형 값들의 부호를 확장하는 것을 허용한 코드를 찾아 냅니다. lint-xarch=v9 옵션은 64-비트 SPARC 컴파일 환경에서만 코드가 수행되는지 확인할때 사용합니다. -xarch=amd64 는 x86 64-비트 환경에서 코드가 수행되는지 확인합니다.

lint 가 주의(warning)을 발생시킨다면 해당 코드의 위치(line number)를 출력하고 문제에 대한 메세지를 출력한 후에 포인터가 연관이 됐는지 안됐는지에 대해서 알려 줍니다. 주의 메세지는 또한 연관된 데이타 타입의 사이즈를 가르켜 줍니다. 사용자가 포인터가 연관된것을 알았고 데이타 타입의 사이즈를 알았다면 이제 64-비트 문제점을 발견해서 32-비트와 64-비트간의 데이타 사이즈 차이로 기인하는 문제를 해결할 수 있습니다.

사용자는 주어진 라인 바로 앞에 "NOTE(LINTED(<optional message>))" 를 삽입 시켜서 아래 라인에서 발생하는 주의 메세지를 무시할 수 있습니다. 이것은 lint 가 캐스트와 지정 할당을 하는 특정한 코드 라인들을 무시하도록 할때 유용합니다. "NOTE(LINTED(<optional message>))" 를 사용할때는 매우 주의해야 합니다. 왜냐하면 진짜 문제가 있는 부분을 숨겨버릴수도 있기 때문입니다. 또한 주의할 점은 NOTE 를 사용할때는 반드시 #include<note.h> 를 포함시켜줘야 합니다. lint man 페이지에서 좀 더 자세한 사항을 알아보시기 바랍니다.

일반 정수형의 사이즈 변화로 인한 포인터 사이즈의 변화 검사

ILP32 컴파일 환경에서 일반 정수형과 포인터가 같은 사이즈를 가지고 있었기 때문에, 32-비트 코드는 일반적으로 이러한 가정을 가지고 있습니다. 포인터는 종종 int 혹은 unsigned int 형으로 주소 계산을 위해 캐스트 돼왔습니다. 사용자는 사용자는 포인터를 unsigned long 으로 캐스트할 수 있습니다. 왜냐하면 long 과 포인터 타입은 IPL32, LP64 데이타 타입 모델에서 모두 같은 사이즈를 가지고 있기 때문입니다. 어쨌든 명시적으로 unsigned long 을 사용하는 대신 uintptr_t 를 사용하시기 바랍니다. 왜냐하면 이것이 의도를 좀 더 명확하게 해주고 코드를 좀더 포팅이 가능하도록 만들어주어서 미래의 변화에 대비할 수 있기 때문입니다. uintptr_tintptr_t 를 사용하기 위해서는 #include <inttypes.h> 이 필요합니다.

다음의 예제를 보시기 바랍니다:

char *p;
p = (char *) ((int)p & PAGEOFFSET);
% cc ..
warning: conversion of pointer loses bits

다 음의 버젼은 32-비트와 64-비트 타겟 모두에서 올바르게 동작할 것입니다:

char *p;
p = (char *) ((uintptr_t)p & PAGEOFFSET);

일반 정수형의 사이즈 변화에 따른 Long 정수형의 사이즈 변화 검사

정수형과 long 형이 IPL32 데이타-타입 모델에서는 구분되어지지 않았었기 때문에 현재 존재하는 코드에서도 이러한 가정을 내포한 코드가 존재할 수 있습니다. 정수형과 long 형을 상호 변환시킨 코드를 수정해야 합니다. 그렇게 해서 IPL32와 LP64 데이타 모델의 요구조건을 만족시켜야 합니다. 정수형과 long 형이 IPL32 데이타 모델에서는 둘다 32-비트 이지만 LP64 데이타-타입 모델에서는 long 형만 64비트 입니다.

다음의 예제를 보시기 바랍니다:

int waiting;
long w_io;
long w_swap;
...
waiting = w_io + w_swap;

% cc
warning: assignment of 64-bit integer to 32-bit integer

부호 확장 검사

부호 확장은 64-비트 컴파일 환경으로 전환할때 가장 일반적인 문제점입니다. 왜냐하면 타입 변환과 프로모션 규칙이 조금 애매하기 때문입니다. 부호-확장 문제를 막기 위해 명시적으로 의도된 결과를 얻기 위한 캐스팅을 사용합니다.

왜 부호 확장이 일어나는지 이해하기 위해 ISO C의 변환 규칙(conversion rule)을 이해하는 것이 도움이 됩니다. 대부분의 부호 확장 문제를 일으키는 변환 규칙은 다음과 같은 작업시에 32-비트와 64-비트 컴파일 환경에 영향을 줍니다:

  • 정수 프로모션(integer promotion)

    사용자는 char, short, 열거형 타입 혹은 비트-필드를 사용해서 signed 이든 unsigned이든 정수형을 위한 표현에 사용할 수 있습니다. 만약 정수형이 본래 타입이 담을 수 있는 값을 가지고 있다면 값은 정수형으로 변환됩니다;그렇지 않을경우 값은 unsigned 정수형으로 변환됩니다.

  • signed 와 unsigned 정수형간의 변환

    마이너스 부호를 가지고 있는 정수가 unsigned 정수형으로 혹은 더 큰 타입으로 대입된다면 첫번째로 signed된 좀 더 큰 타입의 값으로 대입된다음에 이것이 unsigned 값으로 변환됩니다.

다 음의 예제가 64-비트 프로그램으로 컴파일 된다면 addr 값은 a.base는 가 unsigned 타입이더라도 부호 확장 됩니다.

%cat test.c
struct foo {
  unsigned int base:19, rehash:13;
};
main(int argc, char *argv[])
{
  struct foo a;
  unsigned long addr;
  a.base = 0x40000;
  addr = a.base << 13; /* Sign extension here! */
  printf("addr 0x%lx\n", addr);
  addr = (unsigned int)(a.base << 13); /* No sign extension here! */
  printf("addr 0x%lx\n", addr);
}

부호 확장은 다음과 같은 규칙이 적용되면서 발생합니다:

  • 구조체 멤버 a.baseunsigned int 비트 필드에서 int 로 변환됩니다. 왜냐하면 정수 프로모션 규칙 때문입니다. 다시 말해 unsigned 19-비트 필드가 32-비트 정수형에 맞으면서 비트 필드가 unsigned 정수형 대신 일반 정수형으로 대입됩니다. 그러므로 a.base << 13int 타입이 되는 것입니다. 만약 결과 값이 unsigned int 가 되면 아무런 문제가 되지 않을 수도 있습니다. 왜냐하면 부호 확장이 아직 발생하지 않았기 때문입니다.
  • 표현 a.base << 13int 타입입니다. 그러나 이것은 long 으로 변환된 다음에 addr 에 할당되기 전에 다시 unsigned long 으로 변환됩니다. 왜냐하면 signed 와 unsigned 정수 프로모션 규칙 때문입니다. 부호 확장은 intlong 으로 변환할때 발생합니다.

그 러므로, 다음의 64-비트 프로그램을 컴파일 하면 다음과 같은 결과를 볼 수 있습니다:

% cc -o test64 -xarch=v9 test.c
% ./test64
addr 0xffffffff80000000
addr 0x80000000
%

 32- 비트 프로그램으로 컴파일 한다면 unsigned long 의 사이즈가 int 의 사이즈와 같으므로 어떠한 부호 확장도 일어 나지 않습니다.

% cc -o test test.c
% ./test
addr 0x80000000
addr 0x80000000
%

구조체 팩킹 검사하기

애플리케이션의 내부 자료 구조체에 존재하는 구멍을 검사합니다; 즉 정렬(alignment) 요구사항을 만족시키기 위한 구조체내 각 필드간의 추가적인 공간을 의미 합니다. 이러한 추가적인 공간은 long 혹은 포인터 필드가 LP64 데이타-타입 모델을 위해 64비트로 늘어나고 int 가 변함없이 32-비트로 남아 있기 ?문에 발생합니다. long 과 포인터 타입은 LP64 데이타-타입 모델에서 64비트로 정렬되고, 추가 공간은 intlong 혹은 포인터 타입 사이에서 나타납니다. 다음의 예제에서 p 는 64-비트 정렬되있고 그러므로 추가 공간은 멤버 k 와 멤버 p 사이에 나타납니다.

struct bar {
  int i;
  long j;
  int k;
  char *p;
}; /* sizeof (struct bar) = 32 bytes */

또한 구조체가 그중에서 가장 큰 사이즈를 가진 멤버에 맞게 정렬되므로 위의 구조체에서 추가 공간은 멤버 i 와 멤버 j 사이에 나타납니다.

구조체를 재팩킹할때 다음과 같이 long 과 포인터 필드를 구조체의 제일 앞에 배치합니다. 다음의 구조 정의를 고려해 보시기 바랍니다:

struct bar {
  char *p;
  long j;
  int i;
  int k;
}; /* sizeof (struct bar) = 24 bytes */

멤버들 사이즈의 부조화 체크하기

union 의 멤버를 꼭 체크하시기 바랍니다. 왜냐하면 그들의 필드는 IPL32와 LP64 데이타-타입 모델에서 멤버들의 사이즈가 변할 수 있기 때문입니다. 다음의 union에서 멤버 _d 와 멤버 배열 _l 는 IPL32모델에서는 동일한 사이즈를 가지지만 LP64 모델에서는 다른 사이즈를 가집니다 왜냐하면 long 타입이 64비트로 커졌지만 double 타입은 그렇지 않기 때문입니다.

typedef union {
  double _d;
  long _l[2];
} llx_

멤버의 사이즈는 _l 배열 멤버의 타입을 long 에서 int 타입으로 변경 시켜 줌으로써 조화시킬 수 있습니다.

상수 타입은 상수 표현에서만 사용되어야 함을 확인

정확 도가 떨어지면 몇몇 상수 표현들에서 데이타의 손실을 발생시킬 수도 있습니다. 상수 표현에서는 데이타의 정확한 타입을 꼭 명시하시기 바랍니다. 각 정수형 상수의 타입을 {u,U,l,L} 을 통해 지정해 줍니다. 또한 캐스트를 통해서도 상수 표현의 타입을 캐스트해 줄 수 있습니다. 다음의 예제를 참고 바랍니다:

int i = 32;
long j = 1 << i; /* j will get 0 because RHS is integer expression */

위 의 코드는 의도된대로 동작하기 위해 다음 처럼 상수의 타입을 지정할 수 있습니다:

int i = 32;
long j = 1L << i; /* now j will get 0x100000000, as intended */

포맷 문자열 변환 확인

printf(3S), sprintf(3S), scanf(3S), 그리고 sscanf(3S) 의 포맷 문자열이 long 혹은 포인터 매개변수들을 담을 수 있는지 확인하시기 바랍니다. 포인터 매개변수를 위해 포맷 스트링에 주어진 변환 작업은 32-비트 그리고 64-비트 컴파일 환경에서 반드시 %p 가 되어야 합니다. long 매개변수는 long 사이즈 를 위한 l 이 반드시 사용되어야 합니다.

또한 sprintf 의 첫번째에 전달되는 버퍼의 크기가 long 과 포인터의 크기 변화에 따라 충분한 공간을 가지고 있는지도 확인해 봐야 합니다. 예를 들어 포인터는 IPL32 데이타 모델에서 8개의 16진수 숫자로 표현되지만 LP64 데이타 모델에서는 16개로 확장되기 때문입니다.

sizeof() 함수의 리턴 값은 unsigned long

LP64 데이타-타입 모델에서 sizeof()unsigned long 형의 유효 타입을 가지고 있습니다. 만약 sizeof()int 형, 혹은 int 형으로 할당되거나 캐스트 되서 매개변수에 전달되면 데이타의 손실이 발생할 수 있습니다. 이것은 보통 매우 큰 long 배열을 가지고 있는 대형 데이타베이스 프로그램에서 문제가 될 수 있습니다.

바이너리 인터페이스 데이타를 위해 포팅 가능한 데이타 타입 혹은 고정길이 정수형 사용

32-비트 와 64-비트 버젼의 애플리케이션에서 자료 구조체를 공유하기 위해서는 ILP32 와 LP64 프로그램사이에서 일반적인 길이를 가지는 데이타 타입을 가지고 있도록 고수해야 합니다. long 데이타 타입과 포인터의 사용을 회피해야 합니다. 또한 32-비트, 64-비트 애플리케이션에서 사이즈가 달라진 라이브러리에서 유래한 데이타 타입들을 사용하지 말아야 합니다. 예를 들어 <sys/types.h> 에 정의된 다음의 타입들은 ILP32, LP64 데이타 모델에서 사이즈가 변화되었습니다:

  • clock_t, 클럭틱에서의 시스템 시간을 나타냄
  • dev_t, 디바이스 번호를 위해 사용됨
  • off_t, 파일 사이즈와 오프셋을 위해 사용됨
  • ptrdiff_t, 두개의 포인터의 주소를 뺀 결과를 위한 signed 정수형 타입
  • size_t, 메모리내의 오브젝트의 바이트형식의 주소를 나타냄
  • ssize_t, 바이트의 수를 리턴하거나 에러를 나타낼때 함수에서 사용됨
  • time_t, 시간을 초 단위로 카운트

<sys/types.h> 에서 유래한 데이타 타입들은 내부적 데이타로 사용하기에 좋습니다. 왜냐하면 코드를 데이타-모델 변화에서 독립시켜주기 때문입니다. 어?든 이러한 타입들의 사이즈들은 데이타 모델에 따라 바뀌므로 32-비트와 64-비트 애플리케이션에서 공유되는 데이타들 혹은 데이타 사이즈가 반드시 고정되어야 하는 상황에서는 이러한 데이타형을 쓰지 않기를 권장합니다. 또한 위에서 언급한 sizeof() 의 변화에 따라 코드에 어떠한 변경을 가하기 전에는 반드시 정확도의 손실에 따른 프로그램에 영향을 고려해야 합니다.

바이너 리 인터페이스 데이타를 위해 <inttypes.h> 에 있는 고정 길이 정수형 타입의 사용을 고려해 보시기 바랍니다. 이러한 타입들은 다음과 같이 외부적인 바이너리 표현을 위해 좋습니다:

  • 바이너리 인터페이스 명세
  • 디 스크상의 데이타
  • 데이타의 선(wire) 상에서
  • 하드웨어 레지스터
  • 바이너리 자료 구조

부작용 고려하기

타입의 변화는 원치 않는 64-비트 변환을 야기 할 수 있습니다. 예를 들어 intssize_t 를 리턴하는 함수를 호출하는 호출 함수를 자세히 살펴보시기 바랍니다.

long 배열이 퍼포먼스에 미칠수 있는 영향을 고려하기

long 혹은 unsigned long 타입의 거대한 배열은 LP64 데이타-타입 모델에서 심각한 퍼포먼스 저하 현상을 야기할 수 있습니다. 이러한 타입은 엄청나나 캐쉬 미스를 야기 하고 좀 더 많은 메모리를 소비하기 때문입니다. 그러므로 intlong 대신 사용가능하다면 intlong 대신 사용할 것을 권장합니다. 이것은 또한 포인터의 배열을 사용하는 대신 int 타입의 배열을 사용하기를 권장합니다. 몇몇 C 애플리케이션에서는 LP64 데이타-타입 모델로 변환뒤에 엄청나게 퍼포먼스가 저하된 경우가 있었습니다. 왜냐하면 매우 많은 수의 대규모 포인터 배열을 사용하고 있었기 때문입니다.

 
 
원 본출처 : http://blog.sdnkorea.com/blog/138

+ Recent posts