빅 엔디언(Big-Endian)과 리틀 엔디언(Little-Endian): 차이와 사용 사례

Endian에 대한 기초 지식

아마도 네트워크 프로그래밍을 조금 해보셨다면 “Little-Endian”과 “Big-Endian”이라는 용어를 들어보았을 것입니다. 때로는 “order byte” 또는 “byte order”라고도 부릅니다.

간단히 말해서 엔디언(Endian)은 컴퓨터에서 데이터가 저장되는 순서를 나타냅니다. 컴퓨터에서 데이터 저장은 일반적으로 바이트(byte) 단위로 이루어집니다. 그러나 이 바이트 단위 저장 시, 각 제조업체(CPU)에 따라 저장 순서가 다를 수 있습니다. 예를 들어, 우리가 자주 사용하는 32비트 정수의 경우, 4바이트 데이터가 한 번에 저장되는데, 이때 가장 낮은 바이트부터 저장하는 방식과 가장 높은 바이트부터 저장하는 방식이 존재합니다. 전자를 리틀 엔디언(Little Endian)이라고 하고, 후자를 빅 엔디언(Big Endian)이라고 합니다.

데이터 방식

리틀 엔디언(Little Endian) 저장 방식은 다음과 같이 데이터를 저장합니다.

I: 32비트 정수

|  1바이트  |
+-----------+-----------+-----------+-----------+
|    I1     |    I2     |    I3     |    I4     |   
+-----------+-----------+-----------+-----------+

 addr A   addr A+1      addr A+2     addr A+3

반면, 빅 엔디언(Big Endian)은 데이터를 다음과 같이 저장합니다.

I: 32비트 정수

|  1바이트  |
+-----------+-----------+-----------+-----------+
|    I4     |    I3     |    I2     |    I1     |   
+-----------+-----------+-----------+-----------+

 addr A   addr A+1      addr A+2     addr A+3

두 가지 방식을 비교해보면 데이터가 서로 반대 순서로 저장되는 것을 알 수 있습니다.

실제적인 예를 살펴보겠습니다. 리틀 엔디언을 사용하는 가장 대표적인 CPU는 Intel 계열 CPU이며, 빅 엔디언을 사용하는 가장 대표적인 CPU는 Sparc 계열 CPU입니다.

엔디언 테스트를 위해 간단한 코드를 작성해보겠습니다.

예제

#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc, char **argv)
{
    int fd;
    int data = 123456789;
    char c[4];
    fd = open("test_bin", O_CREAT|O_WRONLY);

    write(fd, (void *)&data, sizeof(int));
    memcpy(c, (void *)&data, sizeof(int));

    close(fd);
}

이 프로그램은 “test_bin”이라는 이름의 파일을 열어 정수형 데이터를 저장합니다. 우리는 이 프로그램을 Intel Linux와 Sparc Solaris에서 실행시켜 결과를 비교해보겠습니다. Solaris에서 위 프로그램을 컴파일하고 실행한 후 생성된 “test_bin” 파일을 리눅스로 가져와서 테스트하겠습니다(동일한 환경에서 테스트하면 혼동을 피할 수 있습니다).

Linux에서 위 프로그램을 실행하고 생성된 파일을 “test_bin_linux”로 이름을 바꾸고, Solaris에서 생성된 파일은 “test_bin_solaris”로 이름을 바꾼 후 “od(1)” 프로그램을 사용하여 값들을 확인해보았습니다.

[root@coco endian]# od -x test_bin_linux 
0000000 cd15 075b
0000004
[root@coco endian]# od -x test_bin_solaris 
0000000 5b07 15cd

위 결과를 보면 리틀 엔디언(Little Endian)과 빅 엔디언(Big Endian)의 차이를 쉽게 이해할 수 있습니다. 데이터 저장 방식이 바이트 단위로 서로 반대임을 알 수 있습니다.

Endian이 실제 프로그래밍 환경에서 중요도

엔디언(Endian)은 데이터 통신 시스템에서 중요한 역할을 합니다. 엔디언은 컴퓨터의 CPU에 따라 다르며 데이터를 주고받을 때 올바른 순서로 해석하는 데 필요합니다. 하나의 시스템에서만 프로그래밍을 한다면 엔디언에 대해 걱정할 필요가 없습니다. 그러나 다른 엔디안을 사용하는 시스템 간 통신을 고려해야 할 때 엔디언에 주의해야 합니다.

네트워크 상에서 발생하는 문제

서버와 클라이언트 프로그램을 만들어보겠습니다. 서버 프로그램은 Sparc Solaris에서, 클라이언트 프로그램은 Intel Linux에서 작동하도록 설정하겠습니다.

다음은 서버 프로그램입니다.

서버 프로그램

#include <sys/time.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    int server_sockfd, client_sockfd, sockfd;
    struct sockaddr_in clientaddr, serveraddr;

    int fd_num;

    int state, client_len;
    int i, maxi, maxfd;
    int data;

    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sockfd == -1)
    {
        perror("socket error : ");
        exit(0);
    }

    bzero(&serveraddr, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_port = htons(atoi(argv[1]));

    if (bind (server_sockfd, (struct sockaddr
             *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("bind error ");
        exit(0);
    }

    if (listen(server_sockfd, 5) < 0)
    {
        perror("listen error : ");
        exit(0);
    }

    client_sockfd = accept(server_sockfd, (struct
                    sockaddr *)&clientaddr, &client_len);
    read(client_sockfd, (void *)&data, sizeof(int));
    printf("%d\n", data);
    close(client_sockfd);
}

다음은 클라이언트 프로그램입니다.

클라이언트 프로그램

#include <sys/time.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int main(int argc, char **argv)
{
    int client_sockfd;
    struct sockaddr_in clientaddr;
    int data = 123456789;
    int client_len;

    client_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    clientaddr.sin_family = AF_INET;
    clientaddr.sin_addr.s_addr = inet_addr("192.168.100.190");
    clientaddr.sin_port = htons(atoi(argv[1]));

    client_len = sizeof(clientaddr);

    if (connect(client_sockfd, (struct sockaddr *)&clientaddr, client_len) < 0)
    {
        perror("Connect error : ");
        exit(0);
    }

    write(client_sockfd, (void *)&data, sizeof(int));
    close(client_sockfd);
}

클라이언트는 123456789를 보내고 서버는 이 값을 받아서 출력하는 간단한 작업을 합니다. 그러나 서버에서는 원하는 값과는 다른 값을 출력합니다. 그 이유는 데이터의 바이트 순서가 다르기 때문입니다.

이 문제를 해결하려면 어떻게 해야 할까요? 인터넷에서는 시스템마다 엔디언 차이가 존재하기 때문에 엔디언을 하나로 통일시키는 것은 불가능합니다. 대신 네트워크 통신 시 모든 데이터를 공통 엔디언으로 변환한 다음 전송하는 방법을 사용합니다. 그러면 데이터를 수신하는 측에서도 공통 엔디언으로 변환하여 사용할 수 있습니다.

C 언어는 이러한 바이트 순서 문제를 해결하기 위한 함수를 제공합니다. 이러한 함수는 호스트 바이트 순서를 네트워크 바이트 순서로 변경하거나, 네트워크 바이트 순서를 호스트 바이트 순서로 변경하는 두 가지 역할을 합니다.

#include <netinet/in.h>

unsigned long int htonl(unsigned long int hostlong);
unsigned short int htons(unsigned short int hostshort);

unsigned long int ntohl(unsigned long int netlong);
unsigned short int ntohs(unsigned short int netshort);

htonlhtons 함수는 호스트에서 네트워크로, ntohlntohs 함수는 네트워크에서 호스트로 바이트 순서를 변경합니다.

그렇다면 위의 코드를 어떻게 수정해야 할까요? 데이터를 보내는 측에서는 htonl 함수를 사용하여 호스트 바이트 순서를 네트워크 바이트 순서로 변경하고, 데이터를 받는 측(서버)에서는 ntohl 함수를 사용하여 네트워크 바이트 순서를 호스트 바이트 순서로 변경하면 됩니다.

클라이언트 코드를 다음과 같이 수정합니다.

data = htonl(data);
write(client_sockfd, (void *)&data, sizeof(int));

서버 코드를 다음과 같이 수정합니다.

printf("%d\n", ntohl(data));
close(client_sockfd);

프로토콜 개발시 해결책

바이트 순서 함수를 사용하여 리틀 엔디언(Little Endian) 및 빅 엔디언(Big Endian) 문제를 해결했지만, 이 방법은 번거로울 수 있습니다. 데이터를 전송할 때마다 엔디언 변환을 수동으로 해주어야 하며, 실수로 변환을 빼먹을 경우 문제가 발생할 수 있습니다.

네트워크 패킷 전송시 데이터를 바이트 단위로 전송하는 방법도 있습니다. char를 사용하는 것입니다. char는 1바이트 크기이므로 바이트 순서에 대해 걱정할 필요가 없습니다. 따라서 데이터 통신 시 char만 사용하여 통신하는 경우도 있습니다. 예를 들어, 12345678을 정수형으로 전송하는 대신 문자열로 변환하여 전송하는 것입니다. 이렇게 하면 엔디언 문제에 대해 걱정할 필요가 없습니다.

엔디언 체크하기

시스템의 엔디언을 간단한 코드로 확인할 수 있습니다.

int main()
{
    int i = 0x00000001;
    if( ((char *)&i)[0] )
        printf( "리틀 엔디언\n" );
    else
        printf( "빅 엔디언\n" );
}

결론

이상으로 엔디언(Endian)에 대한 기초 지식을 살펴보았습니다. 데이터 통신 시 바이트 순서에 주의해야 할 때가 있으며, 네트워크 통신에서는 데이터를 공통 엔디언으로 변환하여 전송하는 것이 중요합니다. 데이터 통신 방식을 선택할 때 엔디언을 고려하여야 하며, 엔디언 변환 함수를 사용하여 데이터를 올바른 순서로 전송 및 수신해야 합니다.

Leave a Comment