4.2.5) 데이터 세그먼트
4.2.5.1) 데이터 세그먼트의 레이블
데이터 세그먼트도 레이블(label)을 갖는다. 그런데 이 경우 레이블이라는 이름보다는 라벨이라고 읽는 편이 이해하기 수월할 수 있다. 왜냐하면 데이터 세그먼트의 레이블이 실제 메모리에 붙어서 해당 메모리를 가리키는 라벨의 역할을 하기 때문이다(사실 코드 세그먼트의 레이블도 코드의 주소 값에 대한 라벨로 보는 편이 옳다). 즉 라벨 그 자체는 가리키는 메모리의 주소 값을 나타낸다. 코드 세그먼트와 다른 점은, 데이터 세그먼트의 데이터에 라벨을 붙일 때는 콜론(:)을 사용하지 않는다는 점이다. 예를 들어 값이 1인 바이트에 lbl이라는 별명, 즉 라벨을 붙이려면 다음과 같이 한다.
lbl db 1
이제 데이터 세그먼트에서 데이터를 정의하는 방법을 알아보자. 앞으로 ‘lbl 라벨이 붙은 데이터 세그먼트의 메모리’를 단순히 lbl이라고 하겠다.
4.2.5.2) 데이터를 정의하고 사용하기
데이터 정의문(data definition statement)은 메모리에 데이터를 정의할 때 사용한다. 구문은 다음과 같다.
[label] directive initializer[, initializer]... |
이는 말로 설명하는 것보다 예제를 보는 것이 이해가 빠르다. 다음은 “PC 어셈블리어” 문서에 소개된 예제를 발췌하여 정리한 것이다. 바로 위의 절과 함께 설명하겠다.
dataseg.asm |
%include 'handy/handy.inc'
; 데이터 세그먼트의 시작을 알리는 segment .data 지시어입니다. segment .data L1 db 0 ; l1의 바이트 값을 0으로 설정 L2 dw 1000 ; l2의 워드 값을 1000으로 설정 L3 db 110101b ; l3의 바이트 값을 110101_2로 설정 L4 db 17o ; l4의 바이트 값을 17_8로 설정 L5 dd 1A92h ; l5의 더블워드 값을 1A92_16으로 설정 L6 resb 1 ; l6을 1바이트 메모리로 정의하고 초기화하지 않음 L7 db "A" ; l7의 바이트 값을 문자 A의 ASCII 값으로 설정 L8 db 0, 1, 2, 3 ; 4바이트를 정의 L9 db "w", "o", "r", "d", 0 ; C 문자열 "word"를 정의 L10 db 'word', 0 ; l10과 같음 L11 times 100 db 0 ; 100개의 db 0을 나열한 것과 같다
... |
NASM에선 큰따옴표와 작은따옴표는 서로 같은 것으로 취급된다. 데이터가 연속적으로 정의되면 그 데이터들은 메모리에 연속해서 존재하게 된다. 따라서 L2는 L1의 바로 다음 메모리에 위치하게 된다. 배열을 정의하려면 L11과 같이 times 지시어를 이용한다.
4.2.5.1절에서 라벨이 메모리의 주소 값이라고 말했다. C에서는 주소 값을 이용해 해당 주소가 가리키는 값을 * 연산자를 이용해 획득할 수 있었다. NASM에서는 * 대신 대괄호(‘[’, ‘]’)를 이용한다. 그 방법은 다음과 같다.
dataseg.asm |
...
; 코드 세그먼트의 시작을 알리는 segment .text 지시어입니다. segment .text global _main _main: push ebp mov ebp, esp
mov al, [L1] ; al에 L1에 위치한 데이터를 대입한다 mov eax, L1 ; eax = L1에 위치한 바이트의 주소 mov [L1], ah ; L1에 위치한 바이트에 ah를 대입한다 mov eax, [L6] ; L6에 위치한 더블워드를 eax에 대입한다 add eax, [L6] ; eax = eax + L6에 위치한 더블워드 add [L6], eax ; L6에 위치한 더블워드 += eax mov al, [L6] ; L6에 위치한 더블워드의 하위 비트를 ; al에 대입한다
mov eax, 0 mov esp, ebp pop ebp ret |
NASM의 중요한 특징이 이 예제에서 드러난다. 바로 어셈블러가, 라벨이 어떠한 데이터를 가리키고 있는지 전혀 신경을 쓰지 않는다는 점이다. 이는 C 컴파일러가 컴파일 시에 자료형을 검사하는 것과는 대조적이다. 후에는 데이터의 주소 값을 레지스터에 저장하고 C의 포인터 연산을 하듯 코드를 작성하게 되는데 이때도 포인터가 정확하게 사용되는지를 어셈블러가 검사하지 않는다. 이 때문에 어셈블리 프로그래밍은 C언어를 이용한 프로그래밍보다도 오류가 잦아지게 된다.
4.2.5.3) data와 bss
다음과 같은 C 코드를 생각하자.
char *str_ptr = "Hello, world!"; char str_arr[] = "Hello, world!"; int main(void) { char *p = &str_arr[0]; // &str_ptr[0]; p[0] = 'h'; // p가 가리키는 메모리의 첫 번째 바이트를 'h'로 변경합니다. return 0; } |
이 코드는 정상적으로 실행되는 코드다. p가 str_arr을 가리키고 있을 때는 첫 번째 문자는 잘 변경된다. 그렇다면p가 str_ptr을 가리키도록 예제를 수정해보자. 이때는 컴파일 시에는 오류가 발생하지 않지만 실행 시에 오류가 발생한다. 무엇이 문제인가?
결론부터 말하자면, 데이터 세그먼트도 수정 가능한 데이터 세그먼트와 수정 불가능한 데이터 세그먼트가 별도로 존재한다. 기본적으로 C의 전역 변수는 수정 가능한 데이터 세그먼트에 들어간다. const와 같은 한정자를 걸어놓지 않는 한 우리가 마음대로 수정할 수 있으니 당연한 것이다. 따라서 str_ptr 포인터 변수와 str_arr 배열 변수 모두 수정 가능한 데이터 세그먼트에 자리하게 된다.
문제는 str_ptr가 가리키는 “Hello, world!" 문자열은 수정 불가능한 데이터 세그먼트에 자리한다는 점이다. str_arr의 경우는 위와 같이 초기화를 진행하면 str_arr 배열을 위한 별도의 공간을 수정 가능한 데이터 세그먼트에 생성하고 문자열을 복사하므로 당연히 수정이 가능하다. 하지만 str_ptr은? 이 포인터 변수는 수정 불가능한 메모리에 자리한 문자열의 주소를 가리키고 있으니, 위와 같이 값을 수정하려고 하면 오류가 발생하는 것이다.
바로 여기서 data와 bss의 차이를 확인할 수 있다. 수정 가능한 데이터가 들어가는 세그먼트는 bss 세그먼트다. data 세그먼트에는 수정 불가능한 데이터가 들어간다. 즉 이 경우 str_ptr, str_arr는 모두 bss 세그먼트게 저장되고, str_ptr이 가리키는 문자열 "Hello, world!"는 data 세그먼트에 들어간다. 따라서 우리가 컴파일러를 개발할 때는 전역 변수와 정적 변수는 모두 bss 세그먼트에 넣어야 한다.
이와 같이 데이터 세그먼트에 대해 알 수 있었다. 우리는 CIL을 이미 배웠으므로, 이 정도만 이해하면 나머지는jscc를 개발하면서 찾아보면 된다.