예시:
000000004747ff90 00 00 00 00 00 00 00 00
000000004747ffa0 00 00 00 00 00 00 00 00-da ff 47 47 00 00 00 00
000000004747ffb0 e8 ff 47 47 00 00 00 00-ed ff 47 47 00 00 00 00
000000004747ffc0 f7 ff 47 47 00 00 00 00-fb ff 47 47 00 00 00 00
000000004747ffd0 00 00 00 00 00 00 00 00-00 00 61 72 67 73 2d 6d
000000004747ffe0 75 6c 74 69 70 6c 65 00-73 6f 6d 65 00 61 72 67
000000004747fff0 75 6d 65 6e 74 73 00 66-6f 72 00 79 6f 75 21 00
000000004747fff0
이 줄의 첫 바이트 주소는:
0x4747fff0
오른쪽 바이트들은 순서대로 주소가 +1씩 증가한다.
주소 바이트
0x4747fff0 75
0x4747fff1 6d
0x4747fff2 65
...
0x4747fffb 79
0x4747fffc 6f
0x4747fffd 75
0x4747fffe 21
0x4747ffff 00
즉:
0x4747fff0 + 0xb = 0x4747fffb
000000004747fff0 75 6d 65 6e 74 73 00 66-6f 72 00 79 6f 75 21 00
오프셋으로 보면:
offset hex
+0 75
+1 6d
+2 65
+3 6e
+4 74
+5 73
+6 00
+7 66
+8 6f
+9 72
+a 00
+b 79
+c 6f
+d 75
+e 21
+f 00
따라서 0x4747fffb는 이 줄에서 +b 위치다.
Hex dump 자체에는 타입 정보가 없다.
같은 바이트도 어떤 타입으로 보느냐에 따라 다르게 해석된다.
바이트 해석
41 uint8_t 0x41
41 char 'A'
41 00 00 00 uint32 0x00000041
41 00 00 00 char[] "A\0\0\0"
먼저 정해야 하는 것:
몇 바이트를 볼 것인가?
어떤 타입으로 해석할 것인가?
예:
1바이트씩 보면 char[] / uint8_t[]
4바이트씩 보면 uint32
8바이트씩 보면 uint64 / pointer
00 00 00 00 00 00 00 00-da ff 47 47 00 00 00 00
중간의 -는 보기 좋게 8바이트씩 나눈 구분자다.
실제 메모리에 - 바이트가 있는 것은 아니다.
64비트 환경에서는 포인터 하나가 8바이트다.
fb ff 47 47 00 00 00 00
이 8바이트가 포인터 하나일 수 있다.
64비트 환경: pointer = 8바이트
32비트 환경: pointer = 4바이트
포인터도 결국 메모리에 저장된 multi-byte 값이다.
메모리에는 이렇게 보이지만:
fb ff 47 47 00 00 00 00
이 8바이트를 하나의 값으로 해석할 때는 little-endian 규칙 때문에 바이트 순서를 뒤집어서 읽는다.
메모리 바이트: fb ff 47 47 00 00 00 00
값 해석: 0x000000004747fffb
앞의 0은 생략할 수 있다.
0x4747fffb
이 값이 포인터라면 의미는:
주소 0x4747fffb를 가리킨다
핵심:
little-endian은 "주소만" 뒤집는 게 아니다.
2바이트, 4바이트, 8바이트 같은 multi-byte 값을
하나의 숫자로 해석할 때 적용된다.
예:
바이트 타입 값
34 12 uint16 0x1234
78 56 34 12 uint32 0x12345678
fb ff 47 47 00 00 00 00 uint64 0x000000004747fffb
문자열의 경우:
79 6f 75 21 00
이것을 char[]로 해석하면 각 요소가 1바이트다.
79 = 'y'
6f = 'o'
75 = 'u'
21 = '!'
00 = '\0'
따라서 순서대로 읽으면:
"you!\0"
정리:
uint16, uint32, uint64, pointer:
여러 바이트를 하나의 값으로 해석하므로 endian 적용
char[], uint8_t[], raw bytes:
1바이트 단위 요소들의 배열로 해석하므로 뒤집을 대상이 없음
이 부분은 8바이트 포인터들이 연속으로 들어 있는 영역으로 볼 수 있다.
000000004747ffa0 00 00 00 00 00 00 00 00-da ff 47 47 00 00 00 00
000000004747ffb0 e8 ff 47 47 00 00 00 00-ed ff 47 47 00 00 00 00
000000004747ffc0 f7 ff 47 47 00 00 00 00-fb ff 47 47 00 00 00 00
000000004747ffd0 00 00 00 00 00 00 00 00-00 00 61 72 67 73 2d 6d
8바이트씩 끊으면:
주소 8바이트 값 값 해석
0x4747ffa0 00 00 00 00 00 00 00 00 0x0
0x4747ffa8 da ff 47 47 00 00 00 00 0x4747ffda
0x4747ffb0 e8 ff 47 47 00 00 00 00 0x4747ffe8
0x4747ffb8 ed ff 47 47 00 00 00 00 0x4747ffed
0x4747ffc0 f7 ff 47 47 00 00 00 00 0x4747fff7
0x4747ffc8 fb ff 47 47 00 00 00 00 0x4747fffb
0x4747ffd0 00 00 00 00 00 00 00 00 0x0
여기서 argv가 0x4747ffa8부터 시작한다고 보면:
argv 슬롯 주소 저장된 값 의미
0x4747ffa8 0x4747ffda argv[0]
0x4747ffb0 0x4747ffe8 argv[1]
0x4747ffb8 0x4747ffed argv[2]
0x4747ffc0 0x4747fff7 argv[3]
0x4747ffc8 0x4747fffb argv[4]
0x4747ffd0 0x0 argv[5] = NULL
주의:
0x4747ffa8, 0x4747ffb0 ... 은 argv 포인터들이 저장된 위치다.
그 안에 들어 있는 값인 0x4747ffda, 0x4747ffe8 ... 이 실제 문자열 주소다.
argv는 보통 NULL로 끝나는 포인터 배열이다.
따라서 NULL 전까지 포인터가 5개이므로:
argc = 5
단, 여기서 argc 값을 직접 읽은 것은 아니다.
argv 배열의 NULL 전까지 포인터 개수를 세어서 argc를 역산한 것이다.
문자열 영역:
000000004747ffd0 00 00 00 00 00 00 00 00-00 00 61 72 67 73 2d 6d
000000004747ffe0 75 6c 74 69 70 6c 65 00-73 6f 6d 65 00 61 72 67
000000004747fff0 75 6d 65 6e 74 73 00 66-6f 72 00 79 6f 75 21 00
포인터별 문자열:
포인터 값 문자열
0x4747ffda "args-multiple"
0x4747ffe8 "some"
0x4747ffed "arguments"
0x4747fff7 "for"
0x4747fffb "you!"
확인 예시:
0x4747fffb -> 79 6f 75 21 00 -> "you!"
C 문자열은 \0에서 끝난다.
79 6f 75 21 00
y o u ! \0
argc = 5;
argv[0] = "args-multiple";
argv[1] = "some";
argv[2] = "arguments";
argv[3] = "for";
argv[4] = "you!";
argv[5] = NULL;메모리 구조로 보면:
argv 슬롯 주소 저장된 포인터 값 가리키는 문자열
0x4747ffa8 0x4747ffda "args-multiple"
0x4747ffb0 0x4747ffe8 "some"
0x4747ffb8 0x4747ffed "arguments"
0x4747ffc0 0x4747fff7 "for"
0x4747ffc8 0x4747fffb "you!"
0x4747ffd0 0x0 NULL
보통 argv[0]은 프로그램 이름 또는 실행 경로다.
argv[0] = 프로그램 이름
argv[1]부터 = 사용자가 넘긴 인자