어셈블리어 기본 구조
어셈블리어는 명령어와 피연산자로 이루어져 있다.
주로 어셈블리어의 형식은 [명령어] [피연산자]이다.
주요 명령어는 다음과 같다
데이터 이동(Data Transfer) | mov, lea |
산술 연산(Arithmetic) | inc, dec, add, sub |
논리 연산(Logical) | and, or, xor, not |
비교(Comparison) | cmp, test |
분기(Branch) | jmp, je, jg |
스택(Stack) | push, pop |
프로시져(Procedure) | call, ret, leave |
시스템 콜(System call) | syscall |
피연산자에는 상수, 레지스터, 또는 메모리가 올 수 있다.
메모리 피연산자는 []로 둘러싸인 것으로 표현되며, "TYPE" PTR [주소] 와 같이 표현한다.
TYPE에는 BYTE, WORD, DWORD, QWORD 등이 올 수 있다.
메모리 피연산자의 예시)
QWORD PTR [0x8048000] | 0x8048000의 데이터를 8바이트만큼 참조 |
DWORD PTR [0x8048000] | 0x8048000의 데이터를 4바이트만큼 참조 |
WORD PTR [rax] | rax가 가르키는 주소에서 데이터를 2바이트 만큼 참조 |
어셈블리 명령어
데이터 이동🚚
데이터 이동 명령어는 어떤 값을 레지스터나 메모리에 옮기도록 지시한다.
- mov dst, src : src에 들어있는 값을 dst에 대입
mov rdi, rsi | rsi의 값을 rdi에 대입 |
mov QWORD PTR[rdi], rsi | rsi의 값을 rdi가 가리키는 주소에 대입 |
mov QWORD PTR[rdi+8*rcx], rsi | rsi의 값을 rdi+8*rcx가 가리키는 주소에 대입 |
- lea dst, src : src의 유효 주소(Effective Address, EA)를 dst에 저장.
lea rsi, [rbx+8*rcx] | rbx+8*rcx 를 rsi에 대입 |
예제📝: 데이터 이동
레지스터, 메모리 및 코드가 다음과 같을 때, 아래에서 적절한 값을 채우시오.
[Register]
rbx = 0x401A40
=================================
[Memory]
0x401a40 | 0x0000000012345678
0x401a48 | 0x0000000000C0FFEE
0x401a50 | 0x00000000DEADBEEF
0x401a58 | 0x00000000CAFEBABE
0x401a60 | 0x0000000087654321
=================================
[Code]
1: mov rax, [rbx+8]
2: lea rax, [rbx+8]
Code를 1까지 실행했을 때, rax에 저장된 값은 0xC0FFEE이다.
*풀이: mov는 src 주소에 있는 "값"을 dest에 넣으므로, dest인 rax에는 src 주소인 0x401a48에 존재하는 값인 COFFEE가 저장된다.
Code를 2까지 실행했을 때, rax에 들어있는 값은 0x401A48이다
*풀이: lea는 src의 "주소값"을 dest에 저장하므로, dest인 rax에 저장되는 "주소값"은 [rbx+8]인 "0x401a48"이다.
산술 연산❌
산술 연산 명령어는 덧셈, 뺄셈, 곱셈, 나눗셈 연산을 지시한다. 그러나 dreamhack의 이 stage에서는 어셈블리어의 곱셈과 나눗셈에 대해서는 다루고 있지 않으며, 덧셈과 뺄셈만을 다루고 있다.
- add dst, src : dst에 src의 값을 더합니다.
add eax, 3 | eax += 3 |
add ax, WORD PTR[rdi] | ax += *(WORD *)rdi |
- sub dst, src: dst에서 src의 값을 뺍니다.
sub eax, 3 | eax -= 3 |
sub ax, WORD PTR[rdi] | ax -= *(WORD *)rdi |
- inc op: op의 값을 1 증가시킴
inc eax | eax += 1 |
- dec op: op의 값을 1 감소 시킴
dec eax | eax -= 1 |
예제📝: 덧셈과 뺄셈
레지스터, 메모리 및 코드가 다음과 같을 때, 아래에서 적절한 값을 채우시오.
[Register]
rax = 0x31337
rbx = 0x555555554000
rcx = 0x2
=================================
[Memory]
0x555555554000| 0x0000000000000000
0x555555554008| 0x0000000000000001
0x555555554010| 0x0000000000000003
0x555555554018| 0x0000000000000005
0x555555554020| 0x000000000003133A
==================================
[Code]
1: add rax, [rbx+rcx*8]
2: add rcx, 2
3: sub rax, [rbx+rcx*8]
4: inc rax
*풀이:
code 1) rax의 값에 [rbx+rcx*8]의 주소에 있는 값을 더한다
따라서 rax의 값인 0x31337에 [rbx+rcx*8]==0x555555554010 주소에 있는 3이라는 값을 더한다.
rax = rax + 3 == 3133A
code 2) add rcx, 2 == rcx = rcx+2
따라서 rcx의 값은 2가 증가한 '0x4'가 된다.
code 3) sub rax, [rbx+rcx*8]
현재 rax == 3133A이고 [rbx+rcx*8]의 주소에 존재하는 값은 '3133A'이다.
따라서 rax = rax - [rbx+rcx*8]을 하면 rax는 0이 된다.
code 4) inc rax를 하면 현재 rax의 값인 0에 1이 더해지므로
rax == 1이 된다.
논리 연산🤔 - and & or
논리 연산 명령어는 and, or, xor, neg 등의 비트 연산을 지시합니다. 이 연산은 비트 단위로 이루어집니다.
- and dst, src: dst와 src의 비트가 모두 1이면 1, 아니면 0
- or dst, src: dst와 src의 비트 중 하나라도 1이면 1, 아니면 0
예제📝 논리연산 - and, or
레지스터, 메모리 및 코드가 다음과 같을 때, 아래에서 적절한 값을 채우시오.
[Register]
rax = 0xffffffff00000000
rbx = 0x00000000ffffffff
rcx = 0x123456789abcdef0
==================================
[Code]
1: and rax, rcx
2: and rbx, rcx
3: or rax, rbx
*풀이:
code 1) rax의 상위 8비트가 모두 15이므로 rcx와 &연산을 하면 rcx의 상위 8비트는 그대로 유지가 된다.
그리고 rax의 하위 8비트는 모두 0이므로, 하위 8비트의 &연산 결과값은 모두 0이 된다.
따라서 and rax, rcx를 실행하면 rax에는 &연산의 결과값인 '0x1234567800000000'이 저장된다.
code 2) 1과 유사한 케이스이다. 이번에는 &연산의 결과가 상위 8비트는 모두 0으로, 하위 8비트는 rcx의 하위 8비트와 동일하다.
따라서 rbx에는 '000000009abcdef0'이 저장된다.
code 3) 현재 code 1과 code 2를 통해
rax == 0x1234567800000000
rbx == 000000009abcdef0이다.
or rax, rbx를 연산하면 rbx의 상위 8비트가 모두 0이므로 ,상위 8비트는 rax의 상위 비트와 동일하게
rbx의 하위 8비트는 위와 유사한 이유로 rbx의 하위 8비트와 동일한 값이 나오게 된다.
따라서, rbx에는 '0x123456789abcdef0'이 저장되게 된다.
논리 연산🤔 - xor & not
- xor dst, src: dst와 src의 비트가 서로 다르면 1, 같으면 0
- not op: op의 비트 전부 반전
예제📝논리 연산 - xor, not
레지스터, 메모리 및 코드가 다음과 같을 때, 아래에서 적절한 값을 채우시오.
[Register]
rax = 0x35014541
rbx = 0xdeadbeef
==================================
[Code]
1: xor rax, rbx
2: xor rax, rbx
3: not eax
*풀이:
code 1) xor rax, rbx를 실행하면 결과값인 'ebacfbae'가 rax에 저장된다.
code 2) code 1의 풀이와 연계해보자면 xor의 계산 특성에 따라 (rax^rbx)^rbx=rax이므로,
rax는 초기의 rax 값인 '0x35014541'가 된다.
code 3) eax는 rax의 하위 32비트이다. 현재 rax의 하위 32비트에는 '0x35014541'가 저장되어있으므로, 이를 not 연산 시켜주면
'rax == 0xcafebabe'가 된다
비교⚖️
비교 명령어는 두 피연산자의 값을 비교하고, 플래그를 설정한다.
- cmp op1, op2: op1과 op2를 비교
cmp는 두 피연산자를 빼서 대소를 비교합니다. 연산의 결과는 op1에 대입하지 않습니다.
예를 들어, 서로 같은 두 수를 빼면 결과가 0이 되어 ZF플래그가 설정되는데, 이후에 CPU는 이 플래그를 보고 두 값이 같았는지 판단할 수 있습니다.
- test op1, op2: op1과op2를 비교
test는 두 피연산자에 AND 비트연산을 취합니다. 연산의 결과는 op1에 대입하지 않습니다.
예를 들어, 아래 코드에서처럼 0이된 rax를 op1과 op2로 삼아 test를 수행하면, 결과가 0이므로 ZF플래그가 설정됩니다.
이후에 CPU는 이 플래그를 보고 rax가 0이었는지 판단할 수 있습니다.
분기🔀
분기 명령어는 rip를 이동시켜 실행 흐름을 바꿉니다.
⚠️분기문은 여기 소개된 것 외에도 굉장히 많은 수가 존재
- jmp addr: addr로 rip를 이동시킵니다.
- je addr: 직전에 비교한 두 피연산자가 같으면 점프 (jump if equal)
- jg addr: 직전에 비교한 두 연산자 중 전자가 더 크면 점프 (jump if greater)
스택, 프로시져, 시스템 콜은 다음 게시글에서 다루도록 하겠습니다.
'DreamHack: System Hacking > F Stage 2' 카테고리의 다른 글
Quiz: x86 Assembly 1 (0) | 2023.03.31 |
---|---|
Background: x86 Assembly Essential Part(2) 스택, 프로시저, 시스템 콜 (0) | 2023.03.31 |
Quiz: Linux Memory Layout (0) | 2023.03.31 |
Background: Linux Memory Layout (0) | 2023.03.31 |
Quiz: Computer Architecture (0) | 2023.03.31 |