Porting Linux applications to 64-bit systems
이 글은 Porting Linux applications to 64-bit systems 을 참조하여 작성한 글입니다.
더욱 명확한 내용 확인을 원하시는 분들은 위 URL 참고하여 주시기 바랍니다.
지금은 64-bit 시스템이 서버나 데스크탑에서 흔하게 사용되지만 예전엔 아니였습니다. Linux는 64-bit 프로세서들을 사용하기 위한 첫 번째 cross-platform 운영체제들 중의 하나였습니다. 많은 소프트웨어 개발자들은 예전에 개발했던 32-bit 기반 어플리케이션들을 64-bit 환경으로 포팅하기 위한 필요성을 느끼고 있습니다. 64-bit 프로세서의 보급화가 빠르게 이뤄지면서 이 필요성은 지속적으로 중요하게되었죠.
UXIX나 다른 UNIX 계열의 운영체제와 같이, 리눅스도 64-bit 환경에 대해 LP64 표준(Standard)를 사용하고 있습니다. LP64(이하 표준 생략)은 포인터와 long integer는 64-bit이지만 regular integer 자료형은 32-bit를 유지하고 있습니다. 몇몇의 High-Level Language에선 이런 자료형의 크기 차이가 큰 영향을 미치지 않지만, C언와 같이 몇몇의 다른 언어에는 큰 영향을 미치고 있습니다.
32-bit 에서 64-bit 환경으로 Application을 포팅하려는 노력은 아주 작은 노력으로도 가능할수도 있지만, 매우 크고 어려운 노력이 필요할 수 있습니다. 이런 노력의 크기는 어떻게 어플리케이션의 코드가 작성되었으며 유지보수 되어왔는지에 의존할 것입니다. 아무리 잘 작성된 코드가 하더라도 매우 미묘한 이슈들이 문제를 일으킬 수 도 있습니다.
이 글은 이러한 이슈에 대해 논의하고, 어떻게 이 이슈들을 처리할지에 대해 제안하는 글입니다.
Advantages of 64 bits
본론에 앞서 32-bit 환경보다 64-bit 환경이 어떠한 장점이 있는지 살펴봅시다.
32-bit 플랫폼은 database와 같이 규모가 큰 appication 개발자들에게 어려움을 주는 수많은 제약이 있었습니다. 컴퓨터 하드웨어의 향상된 성능을 충분히 이용하고 싶은 개발자들에게 말이죠.
과학분야에서 필요한 연산이 부동소수점 연산(floating-point mathematics)에 의존하는 것에 비해, 금융분야에서 필요한 연산은 좁은 범위의 수를 필요로 하지만 부동소수점보다는 연산의 높은 정확도를 원하고 있습니다. 64-bit 연산은 높은 정확도의 fixed-point 연산을 제공할 수 있습니다.
컴퓨터의 32-bit 주소에 의해 생기는 문제에 대해서도 많은 논의가 이뤄지고 있는데요, 32-bit 포인터는 오직 4GB의 가상 주소 공간(Virtual Address Space)만을 제공하기에 Application 개발자들이 이 제한적인 주소 공간을 극복하기 위해 그 이상을 원한다면 매우 복잡하고 어려운 과정이 필요할 것입니다. 그렇게 문제를 해결한다고 해도 성능이 향상되긴 커녕 점차적으로 성능이 하락하는 상황을 만날 것입니다.
또한 Linux의 date(날짜) 표현에 대해서도 문제가 있습니다. 현재 Linux에서는 1970년 1월 1일을 기준으로 32-bit signed-integer 자료형에 초를 저장하는 방식으로 date를 표현하고 있습니다. 32-bit 크기의 자료형에서는 2038년을 기점으로 값이 음수로 변하는 상황을 맞게됩니다. 그러나 64-bit system에서 date는 signed- 64-bit integer 자료형으로 표현될 것이고 사용가능한 범위가 매우 커지게 되겠죠.
정리하여 64-bit architecture가 가지고 있는 이점은 아래와 같습니다.
1. 64-bit Application은 4 exabyte 크기의 가상 메모리를 직접 접근할 수 있습니다.
2. 64-bit Linux는 4 exabyte까지 파일의 크기를 허용할 수 있습니다. 큰 Database에 접근하는 서버에겐 매우 중요한 이점입니다.
The Linux 64-bit architecture
안타깝게도, C 언어는 새로운 기본 자료형(fundamental data type)을 추가하는 메카니즘이 없습니다.
C언어에서 64bit addressing과 64bit 연산을 지원하는 것은 바인딩을 변경하거나 기존에 존재하는 자료형에 mapping 또는 새로운 자료형(기본 자료형이 아닙니다. C언어에서는 기본 자료형을 추가할 방법이 없습니다.)을 추가하는 것입니다.
아래 표에 64-bit standard에 대해 나열하였습니다. LP64 , LLP64, ILP64 이 3가지 64-bit 표준에서의 차이는 non-pointer data type입니다. 자세히 보시면, non-pointer data type들의 크기가 다른 것을 알 수 있습니다.
|
ILP32 |
LP64 |
LLP64 |
ILP64 |
char |
8 |
8 |
8 |
8 |
short |
16 |
16 |
16 |
16 |
int |
32 |
32 |
32 |
64 |
long |
32 |
64 |
32 |
64 |
long long |
64 |
64 |
64 |
64 |
pointer |
32 |
64 |
64 |
64 |
C언어에서는 하나 이상의 자료형의 크기가 다른 모델에서 변경될 때 Application이 여러가지 영향을 받을 수 있습니다. 나타날 수 있는 2가지 영향에 대해서 알아보겠습니다.
1) Size of data objects
컴파일러는 32 bit 자료형을 64 bit system에서 32 bit 크기로 잡게됩니다(64 bit 자료형은 64 bit 크기로 잡습니다). 구조체(struct)나 공용체(union)과 같은 data object의 크기가 32 bit 시스템과 64 bit 시스템에서 다를 수 있음을 의미합니다.
2) Size of fundamental data types
기본 자료형 간의 관계(크기 등)에서 여러분들은 쉽게 추측할 수 있었지만, 64 bit data-model에선 더 이상 그 추측은 유효하지 않습니다. Application 내에 기본 자료형들간의 관계는 64-bit 플랫폼에서 컴파일 과정 중 에러가 발생할 수 있습니다.
예를 들어 아래의 기본자료형들 간의 크기 관계에서 32 bit 시스템(ILP32 표준)에서는 유효한 걸 모두 알고 계시지만, 다른 64 bit 시스템(LLP64, LP64, ILP64)에서는 유효하지 않습니다.
sizeof(int) = sizeof(long) = sizeof(pointer)
다시 정리하면, 컴파일러는 자료형에 따라 메모리 크기를 할당 및 정리하게 됩니다. 구조체나 공용체에서 할당되는 메모리를 정리하기 위해 'padding'이 각 자료형 사이에 존재하게 됩니다. 만약 아래와 같은 구조체가 있다고 해봅시다.
struct test {
int i1;
double d;
int i2;
long l;
}
아래 표는 구조체 각 멤버에 대해 할당된 메모리크기와 padding을 보여준다.
Structure member |
Size on 32-bit system |
Size on 64-bit system |
struct test { |
|
|
int i1; |
32-bits |
32-bits |
|
32-bits filler |
|
double d; |
64-bits |
64-bits |
int i2; |
32 bits |
32 bits |
|
32-bits filler |
|
long l; |
32 bits |
64 bits |
}; |
Structure size 20 bytes |
Structure size 32 bytes |
Porting from 32-bit to 64-bit systems
이번 장에서 흔히 발생하는 문제에 대해 어떻게 해결하는지 알아봅시다.
먼저, 살펴볼 부분에 대해 나열해보면 아래와 같습니다.
Declarations
Expressions
Assignments
Numeric constants
Endianism
Type definitions
Bit shifting
Formatting strings
Declarations
여러분의 코드가 32-bit 와 64-bit 시스템 모두에서 제대로 동작하기 위해서는 아래와 같은 방식을 따라야 합니다.
정수형 상수를 사용할 때 'L' 또는 'U' 접미사를 적절히 사용해야 합니다. unsigned int 형을 사용하는게 확실한다면 sign으로 선언하는 것을 피하고 32-bit, 64-bit 시스템에서 모두 32bit 자료형이 필요하다면 int를 사용해야 합니다. 다른 자료형은 각 시스템에서 다른 크기를 가지게 될 수 있기 때문이죠.
만약 32-bit 시스템에서는 32-bit 자료형이, 64-bit 시스템에서는 64-bit 자료형이 필요하다면 자료형은 long으로 선언합니다.
또한, character pointer와 character byte들은 unsigned로 선언되어야 합니다. signed로 선언되어 있을 시 부호확장문제(sign extension problem)가 발생할 수 있습니다.
Expressions
C/C++에서 표현식은 기본적으로 연산자 우선순위와 산술 방식은 공통적입니다. 여러분들의 코드에서 표현식을 확실히 하기 위해선 아래 내용을 숙지하고 있어야 합니다..
두 signed int 자료형의 합의 결과는 signed int이며, int와 long의 합은 long으로 표현됨을 알야합니다. int와 double의 합은 double로 표현되며 합 연산 전에 int는 double형으로 변환됩니다. 만약 피연산자 중 하나가 unsigned이고 또 다른 피연산자는 signed int 일때 이 표현식은 unsigned 입니다.
Assignments
64-bit 시스템에서 포인터, int, long 형의 크기가 더 같지 않기 때문에 32-bit 시스템에서 작성된 코드가 문제가 될 수 있습니다.
int 와 long 자료형을 서로 교환적으로 사용하지 마세요. 64-bit 시스템에서 long 자료형은 64-bit의 크기를 가지므로 유효자리수가 짤리고 할당 될 수 있습니다. 아래 예시처럼 코드를 작성하시면 안됩니다.
int i;
long l;
i = l;
또한, int 자료형에 pointer를 저장하는 방식의 코드를 작성하지 마세요. 32-bit 시스템에서는 정상적으로 동작할 지 모르지만 64-bit 시스템에서는 오류가 발생할 수 있습니다. 64-bit에서 pointer는 64-bit의 크기를 가지기 때문에 32-bit 크기의 int 자료형에 할당할 수 없습니다. 예를 들어 아래와 같이 2가지 예시대로 코드를 작성하시면 문제가 발생 할 수 있습니다.
unsigned int i, *ptr;
i = (unsigned) ptr;
int *ptr;
int i;
ptr = (int *) i;
unsigned 와 signed 32-bit 정수형을 함께 사용한 표현식을 signed long 자료형에 할당하는 경우에는 둘 중 하나를 long 자료형으로 타입캐스팅을 하던지 전체 표현식을 타입 캐스팅 한 후 할당해 줘야 합니다.
아래와 같은 코드는 문제를 일으킬 수 있습니다.
long n;
int i = -2;
unsigned k = 1;
n = i + k;
그럴 땐 아래와 같이 타입 캐스팅을 해주시면 됩니다.
n = (long) i + k;
n = (int) (i + k);
Numeric Constants
16진수 상수값은 mask나 특별한 비트 값으로 자주 사용됩니다. 16진수 상수에 접미사를 붙이지 않는다면 unsigned int 형으로써 정의됩니다. 예를들어 0xFFFFFFFFL 은 signed long 형입니다. 32-bit 시스템에서 앞의 예시는 모든 bit들이 set됩니다. 하지만, 64-bit 시스템에서는 오직 아래 32-bit만이 set됩니다. 결과적으로 64-bit 시스템에서는 0x00000000FFFFFFFF으로 표현이 되는 것입니다.
만약 여러분들이 64-bit 시스템에서 모든 bit가 set 되길 원한다면 signed long형의 -1을 설정함으로써 모든 비트를 set 할수 있습니다.
long x = -1L;
또 다른 문제로 MSB(Most Significant Bit)를 set할 때 발생할 수 있습니다. 32-bit 시스템에서 여러분들이 만약 16진수 상수로 0x80000000을 사용하여 MSB를 set하게 되면, 64-bit 시스템에서는 적용되지 않습니다. 그 땐 아래와 같이 해주는 방식이 좋습니다.
1L << ((sizeof(long) * 8) - 1);
Endianism
Endianism은 data를 저장하고 어떻게 byte들을 접근할 지에 대한 방법입니다.
2가지 Endianism이 있습니다. 많이 들어보셨듯이 Little-Endian과 Big-Endian 방식입니다. Little-Endian은 LSB(Least Significant Byte)가 가장 낮은 메모리 주소 쪽에 저장되며, MSG(Most Significant Byte)가 가장 높은 메모리 주소쪽에 저장됩니다. Big-Endian은 Little-Endian과 정 반대입니다. LSB(Least Significant Byte)가 가장 높은 메모리 주소 쪽에 저장되며, MSG(Most Significant Byte)가 가장 낮은 메모리 주소쪽에 저장됩니다.
아래 Table을 보시면, 64-bit long integer의 예제를 보여줍니다.
Table . Layout of a 64-bit long int
|
Low address |
|
|
|
|
|
|
High address |
Little endian |
Byte 0 |
Byte 1 |
Byte 2 |
Byte 3 |
Byte 4 |
Byte 5 |
Byte 6 |
Byte 7 |
Big endian |
Byte 7 |
Byte 6 |
Byte 5 |
Byte 4 |
Byte 3 |
Byte 2 |
Byte 1 |
Byte 0 |
예를들어, 32-bit word 0x12345678이 big-endian 기반에 표현되면 아래와 같습니다.
Table . 0x12345678 on a big-endian system
Memory offset |
0 |
1 |
2 |
3 |
Memory content |
0x12 |
0x34 |
0x56 |
0x78 |
만약 0x12345678이 두개의 word 타입으로 보여준다면 0x1234와 0x5678로 나타날 수 있으며, 아래와 같이 보여집니다.
Table . 0x12345678 as two half words on a big-endian system
Memory offset |
0 |
2 |
Memory content |
0x1234 |
0x5678 |
그러나 Little-Endian 기반에서는 아래와 같이 표현됩니다.
Table . 0x12345678 on a little-endian system
Memory offset |
0 |
1 |
2 |
3 |
Memory content |
0x78 |
0x56 |
0x34 |
0x12 |
유사하게, 두 개의 word 타입으로 나눠도 아래와 같이 표현이 됩니다.
Table . 0x12345678 as two half words on a little-endian system
Memory offset |
0 |
2 |
Memory content |
0x5678 |
0x1234 |
위의 테이블을 보면 Big-Endian과 Little-Endian의 차이를 알 수 있습니다.
Endianism은 Bit mask로 사용되어 지거나, 객체의 간접 포인터 주로의 부분으로 사용될 때 중요합니다.
C / C++에서 우리는 bit field를 사용함으로써 endian 문제를 해결 할 수 있습니다. mask field나 16진수 상수를 사용하는 것보다 bit field를 사용하는 걸 추천합니다.
Type definitions
여러분들의 C / C++ code에서 32-bit 시스템과 64-bit 시스템 사이에서 크기가 변하는 자료형을 사용하지 않는 것을 추천합니다. 사용한다면 명확하게 size가 정의되어 있는 type또는 macro를 사용하면 훨씬 좋을 것입니다.
ptrdiff_t
: 두 포인터의 뺄셈의 결과로 signed int형.
size_t
: unsigned int형으로써 sizeof 연산의 결과. size_t는 malloc(3)과 같은 함수의 parameter나 fred(2)와 같은 함수들의 return 값으로 사용.
int32_t, uint32_t
: 미리 int형의 크기를 정의.
intptr_t , uintptr_t
: int형 타입의 포인터를 저장할 때 사용. void형 포인터를 int형의 유효한 포인터로 표현할 때 사용.
Example 1.
아래 조건에서 sizeof 의 return 값이 64-bit 시스템에서는 32-bit가 잘려서 표현되게 됩니다.
int buffersize = (int) sizeof (something);
이 문제의 해결방법은 return 값을 size_t로 캐스팅하고 buffersize 변수 또한 size_t 타입으로 선언하는 것입니다.
size_t buffersize = (size_t) sizeof (something);
Example 2.
32-bit 시스템에서 int와 long 자료형의 크기는 같습니다. 이 것 때문에 몇몇의 개발자들은 두개의 자료형을 교환하여 사용하는 경우가 있습니다. 하지만 이 것은 문제를 일으킬 수 있습니다. 가끔 pointer에 int 자료형 데이터를 할당하는 경우가 있는데 64-bit의 경우 pointer는 54-bit 이기에 32-bit가 잘려 나가는 문제가 발생합니다. 이러한 경우 intptr_t, uintptr_t와 같은 특별한 type을 사용하여야 합니다.
Bit shifting
type을 정의하지 않은 정수형 상수는 unsigned int형 type입니다. 이렇게 type을 정의하지 않고 정수형 상수를 사용하는 것은 Bit shifting 과정 중에 문제가 발생할 수 있습니다.
예를 들어, 아래 코드에서 a의 최대 값은 31이 될 것 입니다. '1 << a' 코드가 int형 타입이기 때문입니다.
long t = 1 << a;
64-bit 시스템에서 shift를 제대로 하기 위해선 정수형 상수뒤에 접미사를 붙여 표현해 주는 것이 좋습니다.
long t = 1L << a;
Formatting String
printf(3) 와 이와 관련된 함수들은 64-bit 시스템으로 porting하는데 많은 문제의 원인이 됩니다. 예를들어 32-bit 시스템에서 '%d'를 이용한 출력은 int와 long 자료형 모두 정상적으로 동작하지만, 64-bit 시스템에서는 long 자료형에서 32bit가 잘리게 됩니다. 적절한 방법은 long 자료형에서 출력은 '%ld'를 사용하는 것 입니다.
비슷하게 char, short, int 잘형과 같은 작은 크기의 정수형을 printf(3)로 전달할 때 64-bit 크기로 커지게 되고, sign 또한 확장될 것입니다. 아례 예제를 보시면, printf(3)는 pointer를 32 비트로 간주하게 됩니다.
char *ptr = &something;
printf ("%x\n", ptr);
위의 코드는 64-bit 시스템에서 에러가 발생할 것이고 오직 하위 4바이트만을 표시하게 됩니다. 위 문제를 해결하기 위해서 %x가 아닌, '%p'를 이용하여 출력하면 32-bit, 64-bit 시스템에서 모두 정상적으로 동작하게 될 것 입니다. 따라서 아래와 같이 코드를 수정하는 게 맞습니다.
char *ptr = &something;
printf ("%p\n", ptr);
Conclusion
최근에 주요 하드웨어 공급업체들은 다양한 성능, 가치등을 위해 64-bit로 확장을 하고 있습니다. 32-bit 시스템의 제약, 특히 4GB의 가상 메모리 등은 64-bit 시스템 개발에 더욱 주력하게 만들고 있습니다. 64-bit 아키텍쳐를 준수하고 개발하는 방법을 아는 것은 여러분들이 더욱 효율적이고 나은 코드를 작성하는데 도움을 줄 것 입니다.
댓글 달기