이번 챕터에선 컴퓨터에서 메모리에 데이터를 저장하는 방식을 의미하는 바이트 오더링의 리틀 엔디언 표기법과 빅 엔디언 표기법에 대해서 알아보겠습니다.
3.1 바이트 오더링
바이트 오더링이란 데이터를 저장하는 방식을 말한다. 그 방식에는 크게 두 가지가 있는데 바로 "빅 엔디언"과 "리틀 엔디언" 방식이 있습니다.
BYTE b = 0x12;
WORD w = 0x1234;
DWORD dw = 0x12345678;
char str[] = "abced";
총 4개의 크기가 다른 자료형이 있습니다. 각 엔디언 방식에 따라서 같은 데이터를 각각 어떤식으로 저장하는지 비교해보겠습니다.
TYPE | NAME | SIZE | Big Endian | Little Endian |
BYTE | b | 1 | [12] | [12] |
WORD | w | 2 | [12][34] | [34][12] |
DWORD | dw | 4 | [12][34][56][78] | [78][56][34][12] |
char[] | str | 6 | [61][62][63][64][65][00] | [61][62][63][64][65][00] |
BYTE 타입까진 두 방식의 차이가 없지만 2바이트 이상의 크기를 가진 자료형을 저장할 때부터 차이가 나타납니다.
① 빅 엔디언 방식은 데이터를 저장할 때 사람이 보는 방식과 동일하게 앞에서부터 순차적으로 저장합니다.
② 리틀 엔디언 방식은 데이터를 저장할 때 역순으로 저장합니다. 다만 바이트 자체는 정상적인 순서로 저장되며 오로지 2바이트 혹은 4바이트 자료형과 같이 멀티 바이트인 경우 각 바이트가 역순으로 저장되는 것입니다.
③ 또한 문자열은 Endian 형식에 상관없이 동일합니다. 그 이유는 문자열이란 결국 char 배열이기 때문에 각 바이트를 하나씩 연속해서 저장한다고 생각하면 됩니다.
3.1.1 리틀 엔디언 & 빅 엔디언
빅 엔디언은 UNIX 서버에 사용되는 RISC 계열의 CPU에서 많이 사용됩니다. 또한 네트워크 프로토콜에 빅 엔디언이 사용됩니다. 이는 매우 중요한 의미를 가집니다. 애플리케이션 개발에 사용된 데이터를 네트워크로 송수신할 때 엔디언 타입을 변경해야 하기 때문입니다.
하지만 Intel x86 CPU에서 리틀 엔디언 방식을 사용합니다. 데이터를 역순으로 저장시키는 리틀 엔디언 방식 같은 경우에는 산술 연산과 데이터 타입이 확장/축소될 때 더 효율적이라는 장점을 가지고 있습니다.
3.1.2 x64dbg에서 리틀 엔디언 확인
테스트를 위한 간단한 코드입니다.
#include "windows.h"
BYTE b = 0x12;
WORD w = 0x1234;
DWORD dw = 0x12345678;
char str[] = "abcde";
int main(int argc, char *argv[])
{
BYTE lb = b;
WORD lw = w;
DWORD ldw = dw;
char *lstr = str;
return 0;
}
위 코드를 빌드하여 만든 LittelEndian.exe를 x64dbg로 디버깅 합니다. Go to명령[Ctrl + G]으로 401000 주소로 갑니다.
그림 3.1에서 보다시피 main() 함수의 주소는 401000이며 전역 변수들의 주소는 40AC40(b), 40AC44(w), 40AC48(dw), 40AC4C(str)입니다. 이 메모리 영역을 디버거의 데이터 창에서 확인해보겠습니다. dump창에서 Goto 명령[Ctrl + G]을 통해 40AC40으로 갑니다.
변수 w와 dw 값들이 리틀 엔디언 형식으로 저장된 것을 확인할 수 있습니다.
※ 리틀 엔디언의 장점
리틀 엔디언(Little Endian) 방식은 메모리에서 데이터를 저장하고 읽는 방법 중 하나로, 다중 바이트로 이루어진 데이터에서 가장 낮은 바이트를 가장 낮은 주소에 저장하는 방식을 말합니다. 인텔(Intel) CPU를 비롯한 많은 컴퓨터 아키텍처가 리틀 엔디언 방식을 사용하는 이유와 그 장점은 다음과 같습니다:
1. 유연한 데이터 타입 처리
- 리틀 엔디언 방식에서는 다중 바이트로 이루어진 데이터(예: 32비트 또는 64비트 데이터)를 하위 바이트부터 순차적으로 처리할 수 있습니다.
- 예를 들어, 32비트 데이터의 하위 8비트를 사용해야 할 때, 리틀 엔디언에서는 첫 번째 바이트를 바로 읽으면 됩니다. 이는 CPU가 다양한 데이터 타입(8비트, 16비트, 32비트 등)을 유연하게 처리할 수 있게 해줍니다.
- 만약 빅 엔디언(Big Endian)을 사용한다면, 하위 바이트를 읽기 위해 데이터를 다시 재배열하거나 추가적인 계산이 필요할 수 있습니다.
2. 효율적인 숫자 연산
- 리틀 엔디언에서는 덧셈이나 뺄셈 같은 연산에서 자리올림(Carry)을 처리하는 것이 더 자연스럽습니다.
- 예를 들어, 32비트 정수에서 가장 낮은 바이트(LSB)부터 계산을 시작할 때, 리틀 엔디언 방식은 이 과정이 더 효율적으로 이루어질 수 있게 합니다. 이는 특히 저수준 프로그래밍에서 연산 효율성을 높이는 데 기여합니다.
3. 호환성 및 역사적 이유
- 인텔 x86 아키텍처는 역사적으로 리틀 엔디언 방식을 채택해 왔습니다. 이는 당시 마이크로프로세서 설계와 관련된 결정에서 비롯된 것이며, 이후 인텔 아키텍처의 표준이 되었습니다.
- 인텔 CPU가 널리 보급됨에 따라, 소프트웨어와 하드웨어의 광범위한 호환성을 위해 리틀 엔디언 방식이 널리 사용되었습니다. 이는 산업 표준이 되어 새로운 CPU나 시스템 설계에서도 리틀 엔디언 방식을 채택하게 된 이유 중 하나입니다.
4. 메모리 접근의 일관성
- 리틀 엔디언 방식은 메모리 접근의 일관성을 제공합니다. 특히 메모리 덤프나 디버깅 도구에서 특정 값의 하위 바이트를 쉽게 확인할 수 있습니다. 이는 시스템 프로그래머나 디버거가 작업을 수행할 때 유리하게 작용할 수 있습니다.
5. 코드 이식성
- 리틀 엔디언 시스템에서 작성된 코드가 빅 엔디언 시스템으로 이식될 때, 종종 코드의 특정 부분을 수정해야 하는 경우가 있습니다. 그러나 리틀 엔디언 방식은 다른 시스템 아키텍처와의 호환성을 고려할 때, 대부분의 표준화된 시스템에서 널리 사용되기 때문에 코드 이식성 측면에서도 큰 문제가 되지 않습니다.
요약
리틀 엔디언 방식은 특히 하위 레벨에서의 데이터 처리와 연산에서 효율성을 제공하며, 인텔 아키텍처에서 역사적으로 채택된 방식입니다. 이러한 방식은 메모리 접근의 일관성을 제공하고, 다양한 데이터 타입의 유연한 처리에 유리하기 때문에 널리 사용되고 있습니다.
'게임해킹 > 리버싱 핵심원리(나뭇잎책)' 카테고리의 다른 글
7장 스택 프레임 (2) | 2024.09.04 |
---|---|
6장 abex' crackme #1 분석 (0) | 2024.08.30 |
5장 스택 (2) | 2024.08.29 |
4장 IA-32 Register 기본 설명 (1) | 2024.08.17 |
1장 리버싱 스토리 / 2장 HelloWorld 리버싱 (1) | 2024.08.08 |