인사말
요즘 공부를 너무 안해서.. 오랜만에 포스팅 올립니다
최근 너무 오픈소스 프로그램을 통해 감사하게도 편하게 일을하다보니 어느 순간 내가 너무 바보가 되는게 아닌가 싶더라구요..
그래서 틈날 때마다 시스템상 이미 공개된 포맷이나 데이터들을 직접 읽어오고 가공해보고있는데 학생 시절 공부하고 잊고있었던 엔디안 이슈가 ㅎㅎ..
오랜만에 보니 헷갈리는 부분도 있고, 예전 공부할 때는 그냥 지나갔던 부분들에 의문도 생겨서 고찰하는 시간도 가져보고자 합니다~
✔️ LSB, MSB란?
컴퓨터/시스템(OS를 포함한) 구조에서 바이너리(이진) 숫자를 어떻게 표현하고 어떤 형식으로 다룰지는 끊이지 않는 논쟁이죠
사람이 읽기 편하면, 컴퓨터가 불편하고.. 바이너리란 항상 그런 관계랄까..
엔디안을 설명하기 앞서 유사한 개념인 LSB(Least Significant Bit), MSB(Most Significant Bit)를 설명하겠습니다.
LSB, MSB는 직역하면 가장 중요한 bit, 가장 덜중요한 bit?
간단하게 설명하자면 MSB는 가장 큰 숫자를 의미하는 bit, LSB는 가장 작은 숫자를 의미하는 bit를 나타내는 용어입니다.
이 용어가 왜있냐!
사람은 당연하게 "1234"라는 숫자가 있으면 가장 왼쪽이 큰 자리수이고, 오른쪽이 작은 자리수입니다.
근데 컴퓨터도? 당연히 그렇게 생각하리란 법은 없죠
0101(5)라는 바이너리 숫자의 표현형이 있습니다. 이는 사람이 읽기 쉬운 이진법 표현이지만, 컴퓨터는 데이터를 다룰때 어떻게 저장할지는 사실 마음대로죠
2^3 (MSB) | 2^2 | 2^1 | 2^0 (LSB) |
0 | 1 | 0 | 1 |
2^0 (LSB) | 2^1 | 2^2 | 2^3 (MSB) |
1 | 0 | 1 | 0 |
CPU의 레지스터가 바이너리를 칩에 저장한다면 어떤 순서로 저장할지는 사실 설계자 마음이죠
컴퓨터 입장에서 중요한건 MSB와 LSB가 누구냐이지, 사람처럼 꼭 MSB가 왼쪽부터 위치할 필요는 없다는 개념이죠
✔️ 엔디안이란?
MSB와 LSB가 bit에 대한 이야기였다면, 비슷한 개념을 "바이트(8bit)" 세계에서 적용한 개념이라고 생각할 수 있습니다.
엔디안은 CPU 레지스터의 Byte 단위 데이터를 메모리에 어떤 순서로 저장(Save)하고 적재(Load)할 것인가?에 대한 문제를 다룹니다.
"x00123456" 표현형의 4byte(int형)이 있다면, 아래와 같이 메모리 주소에 저장됩니다.
# Big Endian
address | 0 | 1 | 2 | 3 |
value(16진수) | x00 | x12 | x34 | x56 |
binary | 0000,0000 | 0001,0010 | 0011,0100 | 0101,0110 |
# Little Endian
address | 0 | 1 | 2 | 3 |
value(16진수) | x56 | x34 | x12 | x00 |
binary | 0101,0110 | 0011,0100 | 0001,0010 | 0000,0000 |
여기서 중요한건, Endian은 바이트 단위의 order를 결정하는 개념입니다.
⭐️비트가 뒤집혔다고 착각하면 안된다는거죠⭐️, 위 표에서 각 바이트의 순서가 변경됬을 뿐 바이트가 표현하는 숫자는 변하지 않습니다.
비트 수준에서보면 Big Endian은 MSB 형태지만, Little Endian은 LSB는 아니란거!!
---
Little Endian은 바이트 수준에서는 Big Endian의 역순이지만, 비트 수준에서는 역순이 아니다보니 익숙하지 않은 분들에게는 확장되는 개념에서 혼란을 야기할 수도 있죠
하지만 쉽게 생각해보면, 컴퓨터를 구성하는 칩, OS등의 시스템들이 데이터를 다루는 가장 작은 단위는 보통 "Byte"입니다.
대부분(사실상 현대 모든) 시스템에서 메모리 주소도 Byte 단위로 할당됩니다. 위 MSB, LSB 사진에서 2의 승수 자리를 표현했지만, 사실 우리는 1 Byte 내부의 비트가 어떻게 물리적으로 구성되어있는지 궁금해할 필요는 거의 없다는 말이죠. (물론 필요할 때도 있습니다.. 이건 아래에서 설명)
바이너리를 표현할 때 16진수 x00123456 이렇게 존재한다면, 위 표와 같이 byte 마다 저런 숫자가 있구나~ 라고만 이해해도 괜찮죠.
실제로 address 1에 x12라는 숫자가 "00010010" 순서의 비트로 들어갔는지 "01001000" 이렇게 뒤집어서 들어갔는지 심지어, 리볼버 총알집마냥 원형으로 돌아가든지 말든지... 고민할 필요가 없습니다. (고민하는 순간 엔디안 개념이 확장될 때, 내 뇌도 돌아갑니다)
트랜지스터와 같이 비트 데이터를 저장하는 칩은 매번 다르게 설계될 수 있습니다.
중요한건 address 1을 담당하는 칩셋에 너 숫자 몇이야? 라고 물어보면 x12라는 1byte value를 대답해주는게 중요한거지
address1! 너 어떻게 비트를 저장하고있어? 라고 물어봤자 써먹을 곳이 없다는 거죠 (있긴한데 아래에서 설명 예정)
프로그래밍을 해보시면 알겠지만, bit 단위로 데이터를 접근하거나 연산하는 방법 따윈 없습니다. (bit 특정할 주소 자체가 없습니다)
흔히 비트마스킹이라는 기법도 결국 byte 데이터를 bit의 배열처럼 사람이 생각하고 and, or, shift, xor 연산하여 원하는 비트를 변형할 뿐이지, 3번째 bit 알려줘! 라고 질의할 operation 자체가 CPU에 존재하지 않죠. 어차피 모든 연산은 byte 단위로 cpu 레지스터에 적재되어서 실행됩니다. (char, 1바이트 보다 작은 단위를 레지스터에 담을 방법 자체가 없다는 소리죠)
// a가 무슨 숫자인지는 모르지만, 2^2 자리, LSB부터 3번째 bit가 궁금하면? */
char a = ???
char b = 0b00000100 // 3번째만 1, 나머지는 0
thirdbit = a & b // and 연산후, 1보다크면 있는거고 없으면 없는거고..
컴퓨터 구조 세계에서는 바이트가 중요하지, 바이트 내부의 비트들이 어떻게 생겨먹었는지는 크게 신경쓸 필요가 없다는걸 강조하려고 말이 길어졌습니다.
굳이 따지자면, 바이트 내부의 비트들은 MSB 즉 사람이 읽기 편한 순서로 저장하는게 정석이라고 알려져있습니다.
문제는 실제로 생긴게 그런지 아닌지 모른다는거죠. CPU는 결국 byte 단위로 읽기 때문에 컴퓨터 설계자가 외부에 표현형만 MSB로 줄 뿐이지, 물리적으로 뒤집어서 사용하는지 그런걸 알 방법이 없죠
bit 표를 보고 컴퓨터 내부도 그렇게 생겼을거야라는 잘못된 직관이 컴퓨터다운 사고방식에 방해가 된다고 생각해서 강조를 많이 하게됬네요 ㅎㅎ..
---
엔디안의 중요한 점은 여기서 끝나는게 아닙니다.
현대 x86을 비롯한 Linux등의 시스템들은 Little Endian을 사용합니다. 그냥 메모리 작은쪽부터 뒤집어서 저장한다는거 아니야?
라고 착각하면 안됩니다.
Endian 개념은 바이트 단위이면서 "자료형" 좀 더 저수준으로 얘기하자면 메모리 값을 cpu 레지스터에 매핑하는 방식입니다.
그게 그 소리 아니야..? 그냥 리틀 엔디안이면 뒤집어서 넣고, 빅 엔디안이면 안뒤집는다는거 아니야?
address | 0 | 1 | 2 | 3 |
value(16진수) | x56 | x34 | x12 | x00 |
binary | 0101,0110 | 0011,0100 | 0001,0010 | 0000,0000 |
위 표는 x00123456 표현형 숫자가 리틀 엔디안 시스템의 메모리에 저장됬을 때 모습이라고 설명했었죠.
그럼 레지스터는 A + B와 같은 연산을 할때 리틀 엔디안 규칙에 맞게 "x00123456"로 해석될 수 있도록 레지스터에 담아서 연산할 것입니다.
하지만!, 이건 int형 즉 4byte를 기준으로 레지스터에게 연산하라고 했을 때 얘기입니다.
만약 위 메모리의 구조가 4byte가 아니라 char형 길이 4개짜리 배열이면 [ x56, x34, x12, x00] 이렇게 해석되죠
어떤 자료형으로 캐스팅하고 cpu 레지스터에 연산을 명령하냐에 따라 (이게 프로그래밍) 똑같은 메모리 상태더라도 다르게 해석됩니다.
좀 더 예시를 들면, short(2byte) 자료형 2개 배열이라고 생각하면 [x3456, x0012]로 해석되겠죠
✔️ bit 순서가 중요한 곳? Big Endiant는 어디서 쓰여??
위에서 현대 시스템은 어차피 byte 단위로만 연산하니, byte의 표현형이 중요하지 byte 내부에 bit를 MSB로 저장할지 LSB로 저장할지 크게 신경 안써도된다고 했었죠, 굳이 따지자면 MSB로 저장하지만...
하지만 서로 다른 Endiant를 사용하는 시스템이나 네트워크처럼 외부에 비트 스트림 (전기적 신호가 bit로 전달되는)을 사용하면 이때는 비트의 순서가 매우 중요해집니다. 그래서 비트는 MSB 순서인게 사실상 표준입니다.
보통 네트워크에서는 "network byte order"로 바이트를 정렬하는 것이 표준인데요. network byte order가 Big Endiant입니다.
굳이 따지자면, byte 내의 비트는 MSB가 가장 왼쪽에 위치합니다.
Little Endiant는 바이트 단위로는 왼쪽부터 작은 수가 오지만, 바이트 내부의 비트는 정반대인거죠
0x00123456을 메모리에 저장한다면 x56, x34, x12, x00 이렇게 바이트 단위로는 뒤집지만 비트 수준에서 보면 LSB 순서가 되지 않죠
그래서 네트워크에서는 바이트 스트림을 보낼 때, 비트수준에서도 일정한 MSB 순서가 보장되는 Big Endiant를 사용하는게 표준입니다.
근데 이것도 2 바이트 이상의 자료형을 의미하는 데이터를 보낼때나 의미있지,
위에서 중요하게 언급했던, 문자열 (1byte 자료형의 배열)을 보낸다면 Endiant가 의미 없죠 그냥 char형 문자열 하나씩 순서대로 해석하면됩니다.
4byte int형 혹은 Sturcture, Class등 바이트 스트림을 특정 자료형으로 캐스팅할거라면, htonl 이런 함수 써서 Big endiant 바이트를 Little Endiant로 바꾸는게 정석입니다.
안바꾸면 어떻게되냐?? 동일한 Endiant를 쓰는 시스템끼리는 정확히 바이트 스트립을 잘라서 자료형 캐스팅만 했다면, 문제 없습니다.
근데 내가 사용하는 휴대폰이 Big Endiant 쓰는 CPU라면 혼란이 오겠죠
그리고 네트워크는 ip, frame등 패킷의 구조체등을 파싱할 때, 서로 다른 시스템이 통신이 가능하려면 같은 프로토콜을 따라야겠죠
누구는 ip 헤더의 특정 바이트 범위를 Little Endiant로 인코딩하고 누구는 Big Endiant로 해석해서 디코딩하면 파멸적이겠습니다..
✔️ 왜 Little Endiant를 사용할까? + 고찰
위 내용을 보면 Big Endiant가 비트수준에서 방향성을 봐도 자연스럽고 사람이 봐도 자연스럽다는 것을 알 수 있습니다.
그럼 big endiant를 쓰지 왜 x86이나 많은 시스템들이 Little Endiant를 쓸까요?
가장 큰 이유는 컴퓨터가 이해하기 쉬운 구조이지 않나 싶습니다.
CPU의 레지스터에 데이터를 적재하거나 연산할때, 당연히 작은 메모리 주소부터 fetch하는게 자연스러울 것입니다.
또한 덧셈, 뺄셈 같은 연산 논리 연산칩들은 LSB부터 연산해서 MSB 방향으로 가게됩니다.
CPU의 파이프라인 구조등을 생각해보면 LSB 자리를 연산하면서 계속 메모리 주소를 순서대로 병행해서 fetch하면 아주 효율적이겠죠
LSB부터 연산하는게 좋다 + 작은 메모리 주소부터 fetch하는게 좋다
==> 작은 메모리 주소에 작은 숫자 자리의 byte를 넣자..!
다시 말하지만, byte 내부 bit는 MSB 순서입니다. 하지만 크게 안중요합니다.
제가 위에서 말한 LSB는 결국 레지스터에 담겼을 때 상태입니다. 메모리에 어떻게 저장되어있던지 중요하지 않다는 소리죠
CPU 레지스터는 byte 단위로 작은 주소부터 fetch하고, Little Endiant에 따라 레지스터에 배치했을 때, LSB가 먼저 오기만 하면되는거죠
---
여기서 저도 의문이 생기더라고요
그럼 그냥 little Endiant에 bit 순서도 LSB가 작은 메모리 주소에 오도록 처음부터 설계하면 안됬나..?
==> 고민해봤는데 char 배열 같은 건 오히려 읽는 순서가 거꾸로 되는 단점도 있긴합니다 (근데 이정돈 상관 없지 않나..?)
==> 관련해서 여러 글을 찾아봤는데, 결국 발전하면서 레거시 시스템과 호환성을 생각하다보니 이렇쿵 저러쿵 됬다 뭐 그런..
x00123456 숫자를 메모리에 저장한다면?
# 원래 Little Endiant
address | 0 | 1 | 2 | 3 |
value(16진수) | x56 | x34 | x12 | x00 |
binary | 0101,0110 | 0011,0100 | 0001,0010 | 0000,0000 |
# bit 까지 뒤집은 Endiant, 비트 수준에서 LSB 순서가 만족됨
address | 0 | 1 | 2 | 3 |
value(16진수) | x56 | x34 | x12 | x00 |
binary (byte 내에서 뒤집힘) | 0110,1010 | 0010,1100 | 0100,1000 | 0000,0000 |
# 그냥 주소 축을 뒤집으면 Big Endiant처럼 읽을 수 있음
address | 3 | 2 | 1 | 0 |
value(16진수) | x00 | x12 | x34 | x56 |
binary | 0000,0000 | 0001,0010 | 0011,0100 | 0101,0110 |
댓글