디컴파일러를 통해 IR 코드로 변환한 내용을 읽어볼 수 있기 위해 어셈블리어를 공부하기로 했다.
기계어는 기계에 친화적인 언어인만큼 컴퓨터가 바로 인식할 수 있는데, 그 때문에 실행 속도가 매우 빠르다. 그러나 인간이 읽기 어려워 배우기도 어렵고 유지보수도 힘들다. 따라서 컴퓨터가 바로 읽을 수 있는 이진수의 기계어를 인간이 읽을 수 있게끔 보완한 어셈블리어가 만들어졌다.
어셈블리어는 기계어와 1:1 대응이 가능한 컴퓨터 프로그래밍에서의 저급 언어이다.
어셈블리어를 기계어로 변환해주는 것을 어셈블러(assembler)라고 하고, 기계어를 어셈블리어로 변환하는 것을 디스어셈블러(disassembler)라고 한다.
어셈블러와 디스어셈블러는 기계어와 어셈블리어 명령어를 1:1로 대응시켜준다.
컴퓨터 아키텍처마다 사용하는 기계어가 다르므로, 기계어를 변환한 어셈블리어도 각기 조금씩 다르다. 오늘날은 하나의 프로세서 계열에 기반을 두고 있다.
어셈블러에는 4가지 종류가 있다.
MASM : 윈도우
GAS : 리눅스/유닉스
NASM : 윈도우/리눅스/맥
SASM : 윈도우/리눅스
같은 종류의 프로세서만 실행할 수 있고 사전 지식이 꽤 필요하다. 여러가지 구성 요소들을 직접 다룰 수 있다는 장점이 있다.
또한 컴퓨터의 실행 과정을 이해하기 쉽고, 성능에 최적인 프로그램의 작성이 가능하다.
Operand: 피연산자
Opcode: 명령어
표기 방식은 문법에 따라 차이가 나는데, Intel 문법과 AT&T 문법이 있다.
Intel 방식에서 ADD EAX EBX라는 구분을 해석하면 EBX의 값을 EAX에 더하는 것이다.
AT&T 방식에서는 EAX의 값을 EBX에 더한다는 뜻이다.
Intel : 숫자를 그대로 사용한다. (1, 2, ... )
AT&T : 숫자의 앞에 S를 붙여 사용한다. (S1, S2, ... )
Intel : 레지스터의 명칭을 그대로 사용한다.
AT&T : 레지스터의 앞에 %를 붙여 사용한다.
평균적으로는 Intel 문법을 따라간다.
RAX : 64비트 단위의 레지스터
EAX : 32비트 단위의 레지스터. 곱셈과 나눗셈 명령어에서 자동으로 사용된다.
AX : 16비트 단위의 레지스터
AH, AL : 8비트단위의 레지스터
ECX : CPU에 의하여 자동으로 루프카운터 사용
EBP : 고급 수준의 프로그래밍이 아니라면 일반 계산과 데이터 전송에서 사용 금지. 고급언어의 스택에 있는 함수 매개 변수와 지역 변수를 참조하기 위해 사용.
EFLAGS : CPU의 동작 제어, CPU 연산의 결과 반영하는 개별적 2진수 비트들로 구성.
변수 : 메모리의 시작주소와 사용되는 메모리의 크기
변수의 3가지 속성 (시작 주소, 저장된 값, 사용하는 데이터의 크기)
사용할 메모리의 크기는 프로그래머가 결정하지만, 사용할 메모리의 위치는 어셈블러가 변환할 때 결정된다.
a(변수이름) 변수형태 b(변수의 초깃값 혹은 크기와 개수)
a는 main 메모리의 stack을 빌려서 저장한다.
이런 변수들은 Input을 통해 값이 설정된다.
resb 형태 : a변수에 b byte 크기의, b개의 메모리 공간을 확보하겠다.
dw 형태 : b가 0x33일 경우, a의 이름으로 1바이트 1개의 메모리 공간을 확보하며 초깃값은 0x33이다.
db : 1바이트 크기의 변수를 표현한다. 편수의 크기는 db로 표시. 초깃값 설정 시 필요가 없다면 ?로 표기한다.
dw : 2바이트 크기의 변수를 표현한다. 변수의 크기는 dw로 표시. 가장 기본이 되는 변수이다.
이 외에도 dd (double 변수), dq(quad 변수), dt(10 바이트 변수) 등이 있다.
배열이름 변수형태 값1, 값2, 값3
레지스터나 메모리에 있는 값을 화면에 출력하기 위해 사용하는 명령어이다.
PRINT_HEX 바이트수, 레지스터/변수이름 : 16진수 출력
PRINT_DEC 바이트수, 레지스터/변수이름 : 10진수 출력
PRINT_STRING "문자열/변수이름" : 문자열 출력
NEWLINE : 화면의 줄 변경
mov a, b
mov 명령어 : 메모리에 데이터를 보낸다.
GET_DEC a, [b]
INPUT에서 받아온 입력값을 a byte의 크기의 10진수 변수 b로 받아온다.
inc a
a(오퍼랜드)의 값을 1 증가한다.
dec a
a(오퍼랜드)의 값을 1 감소한다.
add a, b
a의 값에 b의 값을 더해준다.
sub a, b
레지스터나 메모리의 값을 뺄 때 사용한다.
imul a, b
a의 값에 b의 값을 곱해준다.
push a
a(오퍼랜드)의 값을 스택에 저장한다.
pop a
스택 최상단 값을 꺼내어 a에 저장한다.
ret
호출했던 바로 다음 지점으로 이동한다.
call proc
프로시저를 호출한다.
jmp proc
특정한 곳으로 분기한다.
int S0x80
OS에 할당된 인터럽트 영역을 system call 한다.
cmp a, b
레지스 a와 레지스터 b의 값을 비교한다.
프로시저를 호출한다.
결과가 0이면 분기한다. (같으면 분기.)
결과가 0이 아니면 분기한다. (같지 않으면 분기.)
결과가 작으면 분기 (부호화 안된 수)
결과가 작으면 분기 (부호화 된 수)
결과가 작거나 같으면 분기 (부호화 안 된 수)
결과가 크면 분기 (부호화 된 수)
결과가 크거나 같으면 분기 (부호화 된 수)
결과가 크면 분기 (부호화 안 된 수)
첫 번째 오퍼랜드와 두 번째 오퍼랜드를 교환한다.
오퍼랜드로 지시된 포트로부터 AX에 데이터를 입력한다.
오퍼랜드가 지시한 포트로 AX의 데이터를 출력한다.
BX:AL이 지시한 테이블의 내용을 AL로 로드한다.
메모리의 오프셋값을 레지스터로 로드한다.
DS로 포인터를 불러온다.
ES로 포인터를 불러온다.
플래그의 내용을 AH의 특정 비트로 로드한다.
AH의 특정 비트가 플래그 레지스터로 전송한다.
플래그 레지스터의 내용을 스택에 쌓는다.
스택으로부터 플래그 레지스터로 뽑는다.
nop
아무런 동작도 하지 않는다.
이 외의 자세한 명령어는 이쪽을 참고하면 좋을 것 같다.
https://coding-factory.tistory.com/650
참고
https://m.blog.naver.com/sol9501/70087101257
https://lucete1230-cyberpolice.tistory.com/40?category=852249