스택을 조작하며 어떤 레지스터 값이 자동으로 증감되는 것

Assembly language

어셈블리어란?

어셈블리어는 기계어와 일대일 대응이 되는 컴퓨터 프로그래밍의 저급 언어다.

기계어와 명령어가 1:1로 대응되는 단어들로 구성되어 있다.

고급언어와 다르게 컴파일을 하면 간단한 명령으로 실행되서 실행 속도가 굉장히 빠르다.

임베디드 시스템 or 커널 프로그래밍, 컴퓨터 보안을 위해서는 어셈블리어를 알아야 한다.

C언어와 어셈블리어 비교

위 두 가지의 결정적인 차이는 '한번의 동작을 몇가지 할 수 있는가?' 이다.

위 사진을 보면 어셈블리는 한번에 한 가지 동작밖에 하지 못한다는 것을 알 수 있다.

코드가 간단명료하기에 한 두줄만 봐서는 어떤 목적으로 만들어 졌는지 알 수 없다. 더군다나 많은 부분을 일일히 지정해 줘야 하므로 코드의 길이가 굉장히 길어진다. 그래서 고급언어를 어셈블리어로 바꾸면 코드의 길이가 늘어난다.

어셈블리어의 명령 포맷

앞으로 사용할 어셈블리는 x86 CPU의 기본 구조인 IA-32를 기본 플랫폼으로 삼아 설명할 예정이다.

(IA-32 또는 x86-32는 인텔의 32비트 마이크로프로세서에서 사용하는 명령 집합 아키텍쳐이다.)

  push        eax

(명령어)     (인자)

어셈블리 코드 한줄한줄은 간단하다. 명령어는 옵코드(opcode)라고 하며, 인자는 오퍼랜드(operand)라고 한다. 명령어를 연산자, 인자는 연산의 대상이기 때문에 피연산자라고도 한다. 오퍼랜드는 컴마로 구분해 여러 개의 인자로 전달할 수 있다.

레지스터

레지스터(Register)란 CPU 내부에 존재하는 다목적 저장 공간이다. CPU는 레지스터와 한몸이기때문에 고속으로 데이터를 처리할 수 있다. 또한 레지스터에는 범용 레지스터, 세그먼트 레지스터, 상태 플래그 레지스터, 명령 포인터 레지스터가 존재한다.

레지스터는 레지스터끼리 계산이 가능하다.

IA-32 레지스터

IA-32 레지스터 종류는 여러가지 인데 예컨대 Basic program execution registers, x87 FPU Registers, MMX registers 등이 있다. 애플리케이션 디버깅의 초급 단계에서는 우선 Basic program execution register에 대해 알아두어야 한다.

1. Basic program execution registers

Basic program execution registers 를 4개의 그룹으로 다시 나눌 수 있다.

Basic program execution registers
  • General Purpose Registers (범용 레지스터) -32 bit 8개
  • Segment Registers (세그먼트 레지스터) -16bit 6개
  • Program Status and Control Register (프로그램 상태와 컨트롤 레지스터) - 32bit 1개
  • Instruction Pointer (명령어 포인터) - 32bit 1개

(레지스터 이름에 E가 붙은 경우 16bit CPU인 IA-16 시절부터 존재하던 16비트 크기의 레지스터들을 32Bit 크기로 확장시켰다는 뜻이다.)

General-Purpose Registers

General-Purpose Registers

위의 4개의 레지스터(EAX, EDX, ECX ,EBX)는 주로 산술연산 명령어에서 상수나 변수 값의 저장용도로 많이 사용되고, 어떤 어셈블리 명령어(MUL, DIV, LODS 등)들은 특정 레지스터를 직접 조작한다.(이러한 명령어가 실행된 이후에는 특정 레지스터들의 값이 변경된다.)

EAX : 가장 많이 쓰는 변수이다. 특히 더하기, 빼기 등 사칙연산에서 주로 사용된다. 또한 함수의 리턴값이나 return 100, return FALSE 등의 코드를 사용할때 100이나 FALSE에 해당하는 값이 EAX에 기록된다.

EDX : 이것 또한 변수의 일종이라 생각하기로 한다. EAX와 마찬가지로 각종 연산에 사용되지만 리턴 값의 용도로는 사용되지 않는다.

ECX : C는 Count의 약자로, 우리가 흔히 for문을 사용할 때 i의 역할을 한다고 생각하면 된다.

다만 일반적으로 우리가 사용하는 fot문에 i와는 다르게 ECX는 미리 루프를 돌 값을 넣어놓는다. 예컨대 3바퀴를 돈다면 먼저 3으로 설정하고 i--;를 하는 것 이다. 카운팅할 필요가 없을 때는 변수로 사용해도 무방하다.

EBX : EBX는 어떤 목적으로 만들어진 레지스터가 아니다. 공간이 더 필요할 때 등 적당한 용도를 프로그래머나 컴파일러가 알아서 만들어서 사용한다. EAX, ECX, EDX가 부족할 때 사용하기도 한다.

아래 4개의 레지스터들은 주로 메모리 주소를 저장하는 포인터로 사용된다.

EBP(Pointer to data on the stack (in the SS segment)) : 함수가 호출 되었을 때 그 순간의 ESP를 저장하고 있다가, 함수가 리턴하기 직전에 다시 ESP에 값을 되돌려줘서 스택이 깨지지 않도록 한다.

ESI(Source pointer for string operations), EDI(Destination pointer for string operations) :

ESI는 시작지 인덱스(Source Index), EDI는 목적지 인덱스(Destination Index)로 사용된다.

쉽게 ESI에서 메모리를 읽어 EDI로 복사한다고 생각하면 된다.

ESI 와 EDI는 특정 명령어(LODS, STOS, REP MOVS) 등과 함께 주로 메모리 복사에 사용된다.

ESP(Stack pointer (in the SS segment)) : 스택 메모리 주소를 가리킨다. 또한 어떤 명령어(PUSH, POP 등)은 ESP를 직접 조작한다.(스택 메모리 관리는 프로그램에서 매우 중요하기 때문에 ESP를 다른용도로 사용하면 안된다.)

ESP는 함수가 호출 되었을 때 그 순간의 ESP를 저장하고 있다가 리턴 직전에 ESP 값을 되돌려 줘서 스택이 깨지지 않도록 한다. 이를 Stack Frame 기법이라고 한다.

Segment Registers

세그먼트(Segment)란 IA-32의 메모리 관리 모델에서 나오는 용어이다. IA-32 보호 모드에서 세그먼트는 메모리를 조각내어 각 조각마다 시작주소, 범위, 접근 권한 등을 부여해서 메모리를 보호한다.

세그먼트 레지스터는 총 6개가 존재한다. 이름은 CS, SS, DS, ES, FS, GS 이고 크기는 16Bit(2Byte)이다.

CS와 SS, DS의 경우 이름 그래도 CS는 Code Segment로 프로그램의 코드 세그먼트를 나타내며, SS는 Stack Segment, DS 는 Data Segment를 나타낸다.

이 외의 ES, FS, GS 세그먼트는 추가적인 데이터 세그먼트 이다.

Program Status and Control Register

플래그(Flag) 레지스터의 이름은 EFLAGS이며 32Bit(4Byte) 크기 이다.

EFLAGS : Flag Register

위 그림은 EFLAGS 레지스터를 나타낸다. 그림을 보면 알 수 있듯이 각각의 비트마다 0이나 1을 가지고 있는데 이는 On/Off 나 Tru/Flase를 의미한다. 일부 비트는 시스템에서 직접 세팅하고 일부는 프로그램에서 사용된 명령의 수행 결과에 따라 세팅된다. (Flag 단어 그대로 생각해서 올라가면 1(on/true) 내려가면 0(off/false)로 이해하면 된다.)

ZF, OF, CF 3개의 플래그는 조건 분기 명령어(ex: JE, JZ, JNZ...)에서 이들 Flag의 값을 확인하고 이에 따라 동작 수행 여부를 결정한다.

*Zero Flag(ZF)

연산 명령 후에 결과값이 0이 되면 ZF가 1(True)로 세팅 된다.

Overflow Flag(OF)

오버플로우는 레지스터나 컴퓨터가 다룰 수 있는 수의 범위에서 빠져 나온 상태를 의미한다

부호 있는 수(signed integer, 실수)의 오버플로우가 발생했을 때 1로 세팅된다. 그리고 MSB(Most Significant Bit-가장 왼쪽에 있는 비트, ex 00001200-> 10001200)가 변경되었을 때 1로 세팅된다.

Carry Flag(CF)

부호 없는 수(unsigned integer, 정수)의 오버플로가 발생했을 때 1로 세팅된다.

  -  signed : 숫자의 양수, 음수 전부 표현 가능한 값 (부호를 가질 수 있어, 양수인지 음수인지 구분 O)
  -  unsigned : 숫자의 양수만 표현 가능한 값 (부호를 가질 수 없어, 양수인지 음수인지 구분 X)

Instruction Pointer

EIP : Instruction Pointer

EIP는 CPU가 처리할 명령어의 주소를 나타내는 레지스터이고 크기는 32Bit(4Byte)이다. 

CPU는 EIP에 저장된 메모리 주소의 명령어(Instruction)을 하나 처리하고 난 후 자동으로 그 명령어 길이만큼 EIP를 증가시킨다.

범용 레지스터들과는 다르게 EIP는 값을 직접 변경할 수 없도록 되어 있어 다른 명령어를 통하여 간접적으로 변경해야 한다. 예컨대 특정 명령어(JMP, CALL, RET)를 사용하거나 Interrupt, Exception을 발생 시켜야 한다.

어셈블리어 필수 명령어

  • Push, POP : 스택에 값을 넣는 것을 PUSH, 스택에 있는 값을 가져오는 것을 POP라고 한다.PUSHAD, POPAD는 모든 레지스터를 PUSH하고 POP하라는 명령어 이다. 오퍼랜드는 push eax와 같이 1개만 있으면 된다.
  • MOV : MOV는 단지 값을 넣는 역할을 한다. 예컨대 MOV eax, 1은 eax에 1을 넣는 코드가 되고, MOV ebx, ecx는 ebx에 ecx를 넣는 코드가 된다.
  • LEA LEA는 MOV와 헷갈릴 수 있는데 간단히 이야기 하면, MOV는 값을 가져오는 것이고 LEA는 주소를 가져오라는 뜻이다. (C언어의 포인터 같은 개념인 듯 하다.)
MOV와 LEA의 차이
  • ADD, SUB : ADD는 src에서 dest로 값을 더하는 명령어이고 SUB는 src에서 dest로 값을 빼는 명령어이다.
  • INT : 인터럽트를 일으키는 명령어이다. 뒤의 오퍼랜드로 어떤 숫자가 나오느냐에 따라 각기 다른 처리가 일어난다. ex) INT 3 -> 옵코드가 0xCC인 DebugBreak()
  • CALL : 함수를 호출하는 명령어로 뒤에 오퍼랜드로 번지수가 붙는다. 해당 번지를 호출하고 작업이 끝나면 CALL 다음번지로 되돌아 온다. 왜냐하면 CALL로 호출된 코드 안에서는 반드시 RET를 만나게 되어 다시 호출한 쪽으로 돌아오기 때문이다.
  • INC, DEC : INC는 i++;, DEC는 i--; 라고 생각할 수 있다.
  • AND, OR, XOR : dest와 src를 연산한다. 여기서 XOR은 dest와 src를 동일한 오퍼랜드로 처리 가능하다. 예컨대 XOR EAX, EAX를 수행할 경우 EAX가 0이 된다. 즉 같은 값으로 XOR을 하면 0이되기 때문에 XOR로 같은 오퍼랜드를 전달했을 때 이것은 변수를 0으로 초기화 하는 효과를 줄 수 있다.
  • NOP : 아무것도 하지 말라는 명령어이다. NOP 미끄럼틀이 소프트웨어 익스플로잇에서 흔히 사용된다.
  • CMP, JMP : 비교해서 점프하는 명령어이다.

Toplist

최신 우편물

태그