학교 NASM 과제

Posted by ho95

NASM(Netwide Assembly)

어셈블리어에는 자주 쓰이는 2가지 타입이 있습니다. NASM(intel 스타일과 유사)과 예전에 정리 했던 GAS가 있습니다.

GAS와 NASM 서로 문법이 조금 다릅니다. GAS에서는 레지스터를 호출할때 %를 붙힌다던지 상수 앞에는 $을 쓴다던지.. 명령어의 오퍼랜드 순서가 다릅니다. 그외에는 큰 차이가 없어 하나를 배우면 나머지 하나도 금방 배울 수 있다고 합니다. 그리고 리눅스에서 실제 실행 파일을 만들때 조금 차이가 있습니다.

어셈블리어로 프로그램을 코딩 하면 용량이 확 줄게 됩니다.

이번 학기에 펌웨어 수업을 들으면서 NASM을 배우고 있습니다.(19.05.10) 교수님께서 과제로 내주셨던 것을 포스팅 해봅니다.

리눅스에서 NASM 사용하기


위에서 GAS와 NASM은 실행 파일 만드는데, 차이가 있다고 했습니다.

리눅스는 기본적으로 GAS (=AT&T 스타일)입니다.

GAS 스타일로 코딩을 한 경우 바로 gcc로 한번에 어셈블하고 실행파일이 생성 되지만 NASM의 경우 일단 yum이나 apt-get으로 nasm을 설치 해주셔야 합니다. 그리고 vi(vim)/nano등으로 소스 파일인 .asm파일을 만들어 주시고 nasm 명령어를 이용해 목적파일(.o확장자)를 만든 후 gcc로 실행 파일을 만들게 됩니다.

목적 파일을 만드는 예시는 터미널 창에서 nasm -f elf64 소스파일.asm입니다. -f는 .o파일의 형식을 뜻하는데, elf64는 64비트 리눅스용 실행파일 이라는 뜻 입니다.

과제1: NASM으로 ‘A~Z’까지 출력하는 프로그램을 만들어라


수업 시간에 기본적인 설명은 들었으나… 막상 어셈블러로 코딩을 하려니 막막했습니다. 게다가… 학교에서 centos 7로 실습할때는 문제 없이 실행파일이 생성 되었는데, 집에서 쓰는 우분투 18.04버전의 gcc에서는 아래 사진과 같은 오류가 떴습니다..

/error.jpg)

구글링 해보니 gcc 설정 떄문에 그러니 -no-pie 옵션을 추가하라고 하네요. 시키는대로 하니 우분투에서도 잘 돌아갑니다.

/hw2_3.01.jpg)

열심히 이리저리 궁리해서 완성 코드입니다.

컴퓨터 구조 시간에 배웠던 메모리 구조를 알고 계신가요? 메모리는 text, data, heap, stack 영역으로 구성되어 있습니다. 사진에 섹션에 보이는 .text, .data가 그 영역들을 나타냅니다.

.data 영역에는 문자 ‘A’가 있는 주소를 msg라고 선언했습니다. db는 define byte로 1바이트 단위를 의미합니다. C언어에서도 char는 1byte입니다. 비슷하지 않나요?…ㅎ 만약 msg db “hello”라고 선언하면 C언어에서는 char msg[6] = “hello”와 같습니다. [6]인 이유는.. ‘\0’인거 아시죠?

.text에는 함수(코드)가 들어갑니다… main: 따로 다른 언어처럼 {} 블록의 개념이 없는 것 같습니다. 이름: 으로 구분 해놓고 jmp(jg,jl,je)로 이곳 저곳 뛰어 다니는 것 같습니다..

main:의 mov edi, 26은 edi에 26을 넣어라는 뜻입니다.(edi=26;) jmp print는 함수 호출과 같다고 보시면 될 것 같습니다.

print:로 가서 mov ecx, msg는 ‘A’가 있는 주소를 ecx에 넣으라는 뜻입니다. C언어의 포인터 느낌입니다. (ecx=&msg)

mov eax, 4부터 int 0x80은 리눅스 시스템 콜을 호출하기 위한 부분입니다. 시스템 콜 번호는 커널 소스의 include/아키텍쳐/unistd.h에서 확인 가능합니다. 4는 모니터에 출력하는 시스템 콜 번호입니다. eax에 4를 넣어놓고, ebx는 한번에 출력할 단위를 나타냅니다. 여기서는 1바이트 단위입니다. 1문자(1byte)이니까요. edx는 ecx를 총 얼마나 읽어올 것인지 하는… 데이터 크기?입니다. 이것도 (1byte) 그리고 int 명령어로 인터럽트를 걸어 시스템 콜 호출합니다. 리눅스에서는 128(0x80)이 시스템 콜 호출하는 인터럽트 번호입니다.

mov eax, [msg]부터 jmp loop는 알파벳 26자를 출력하기 위한 카운트 부분이라고 생각 하시면 됩니다. 레지스터나 변수 앞에 []가 붙게 되면 참조와 같습니다. msg는 ‘A’가 있는 주소를 나타내지만 [msg]로 표현하면 msg의 주소에 있는 ‘A’를 가리키게 됩니다. eax에 ‘A’를 넣는데, 실제로는 ‘A’의 아스키 코드인 0x41이 들어갑니다. 그리고 inc는 +=1과 같습니다.(i++같은) 그래서 eax에 있는 0x41은 1증가 되어 B의 아스키 코드인 0x42가 됩니다. 그걸 다시 msg가 가리키게 합니다. 그리고 다시 반복 하기위해 loop를 호출

dec는 inc의 반대입니다. -=1입니다.(i–) edi에는 맨처음 main에서 26을 넣어 놨습니다. 1 감소 되어 25가 있게 됩니다. 그리고 cmp는 if와 같은 맥락입니다. edi 와 0을 비교 하는데, 실제로는 가상 뺄샘을 해봐서 양수 음수 0으로 구분합니다. jg는 cmp의 결과가 양수이면 참이라는 뜻입니다. 그래서 cmp의 결과가 양수이면 다시 print:로 가게 됩니다. (goto문 같은 느낌) jl은 값이 음수, je는 0일때 참이됩니다. 어떤 느낌인지 아시겠나요?

다시 그렇게 print로 가서 msg의 주소를 ecx에 넣고 시스템 콜 호출해서 출력하고 다시 loop로 점프합니다. 그런식으로 26번 반복하고 A~Z가 출력됩니다. 26번 반복하는 이유는… A부터 Z까지가 26개이기 떄문입니다.

그리고 마지막으로 edi가 0이되면 jg는 거짓이 되어 마지막 호출 했던 print의 loop 다음줄부터 시작하게 됩니다. 그렇게 25번 돌아간 후 메인의 jmp print 다음 부분으로 최종적으로 복귀하고, 1번 시스템 콜인 exit()를 호출하여 프로그램이 종료.

왜 jmp 다음 줄로 돌아 가는가… 다른 언어에서도 그렇지만 함수(메소드)를 호출 할때 스택에 호출한 다음 줄을 복귀 주소로 저장하기 때문에 그렇습니다.

실제 실행 결과는 아래 사진과 같습니다.

/hw2_3.02.png)

A-Z까지 출력 되었습니다. 짝짝짝

사실 한 글자 출력하고 줄 바꿈을 하고 싶었으나… 너무 번거로워져 한 줄에 모두 출력 하였습니다.

과제2:0~9 숫자를 입력 받아 그만큼 줄바꿈을 실행하고 A출력하기

이 과제의 코드도 대부분은 과제1과 같습니다. 입력을 위한 임시buf와 입력 시스템 콜이 출력 된 것이 차이점 입니다.

/hw2_4.01.png)

좀 길죠? .bss는 초기화 되지 않은 전역변수를 의미합니다. resb는 reserve byte의 약자이며 1은 크기를 나타냅니다. 1byte의 공간을 잡아 둔다는 의미 입니다.

main에서 레지스터에 입력을 위한 설정을 해주고 int 0x80으로 read 시스템 콜을 호출합니다.

mov edi, [buf] / sub edi, 0x30은 아스키 코드로 받은 값을 실제 숫자로 바꿔주기 위한 부분입니다. 0x30은 아스키 코드로 0입니다.

jmp printCR로 줄바꿈을 입력 받은 만큼 해주고, jmp printA로 A를 마지막에 출력 해주게 됩니다.

실행 결과는 아래와 같습니다.

/hw2_4.02.png)

비록 간단한 코드이지만 어셈블리어로 코딩 하려니 저에겐 많이 어려웠습니다.

연습만이 살길입니다.