문맥교환(컨텍스트 스위칭)을 연습해 보았다.


main.c

#include <stdio.h>

typedef struct _context
{
  int  efl;
  int  eip;
  int  edi;
  int  esi;
  int  ebp;
  int  esp;
  int  ebx;
  int  edx;
  int  ecx;
  int  eax;
} CONTEXT;

void STST( CONTEXT *stpReg );  // CPU의 정보를 메모리에 저장
void LDST( CONTEXT *stpReg );  // 메모리에 저장된 CPU 정보를 재등록
void PrintRegister( CONTEXT *stpReg );

int main()
{
  CONTEXT  stReg =  { 0, };
  PrintRegister( &stReg );

  STST( &stReg );
  PrintRegister( &stReg );

  getchar();

  LDST( &stReg );

  printf( "Kernel Panic\n" );
  return 0;
}

void PrintRegister( CONTEXT *stpReg )
{
  printf( "┌──────RegisterStatus──────┐\n" );
  printf( "│EAX : 0x%08X\t", stpReg->eax );
  printf( "ECX : 0x%08X│\n", stpReg->ecx );
  printf( "│EDX : 0x%08X\t", stpReg->edx );
  printf( "EBX : 0x%08X│\n", stpReg->ebx );
  printf( "│ESP : 0x%08X\t", stpReg->esp );
  printf( "EBP : 0x%08X│\n", stpReg->ebp );
  printf( "│ESI : 0x%08X\t", stpReg->esi );
  printf( "EDI : 0x%08X│\n", stpReg->edi );
  printf( "│EIP : 0x%08X\t", stpReg->eip );
  printf( "EFL : 0x%08X│\n", stpReg->efl );
  printf( "└───────────────────┘\n\n" );
}



monitor.asm

.386

.MODEL FLAT


PUBLIC _STST ; Store Status

PUBLIC _LDST ; Load Status


.CODE

_STST PROC NEAR32

PUSH EBP

MOV EBP, ESP


PUSHFD


AND DWORD PTR [EBP-4], 0FFFFFEFFh


MOV ESP, [EBP+8]


ADD ESP, 40


PUSHAD


ADD ESP, 16

;Old EBP, RA, 인자, Main과의 경계

MOV EAX, EBP

ADD EAX, 8


PUSH EAX ; 메인 ESP


PUSH [EBP] ; Old EBP


SUB ESP, 8


PUSH [EBP+4] ; EIP(Return Address)

PUSH [EBP-4] ; EFL


MOV ESP, EBP

POP EBP

RET

_STST ENDP


_LDST PROC NEAR32


MOV ESP, [ESP+4] ; ESP = &Context


POPFD ; EFL = Context.EFL

; 플래그 레지스터에 값을 넣음


POP EAX ; EAX = Context.EIP

; 예전 EIP를 EAX 저장


MOV EBX, ESP ; EBX = Current ESP

; 현재 ESP 백업


MOV ESP, [ESP+12] ; ESP = Old ESP

; 과거 ESP로 점프


PUSH EAX ; Save Old EIP

; 과거 ESP에 EAX를 푸쉬하여 RA덮어씀


MOV ESP, EBX ; ESP = Current ESP

; 백업해둔 ESP를 다시 복원


POPAD ; ESP를 제외한 모든 값을 복원


MOV ESP, [ESP-20] ; Old ESP값을 복원

SUB ESP, 4 ; ESP를 RA위로 올려주어야 한다(RET이 POP하기때문)


RET

_LDST ENDP

END



결과


실행 순서

1

CPU 정보 구조체 선언 및 초기화

2

구조체 내용 출력

3

구조체에 CPU 정보 저장

4

구조체 내용 출력

5

구조체에 저장된 정보를 CPU 레지스터에 넣음

(STST의 RA가 가르키는 곳, 즉 4로 되돌아감)

6

(실행되지 않음)

Kernel Panic 출력


 실제로는 STST가 수행된 직후 상황을 강제로 재구현하는 방식이다. 원래라면 LDST를 실행한 후 printf를 실행하여 Kernel Panic이 출력되며 종료되어야 하나, LDST로 CPU정보를 STST에서 저장한 값으로 바꾸어버렸기 때문에 무한루프를 돌게 된다.



 pushad로 넣은 값을 읽어들일 때 popad를 쓸 수 있는데, popad는 ESP의 값을 쌩깐다는 점에 유의한다.(ESP의 위치는 이동하지만 값은 유지되어있다) POP은 ESP에서 이루어지는데 ESP가 도중에 변경되면 지장을 줄 수 있다는 문제때문인 듯 하다.


 끝에 SUB ESP, 4를 해주는 이유는 RA의 위로 이동하여야 제대로 RET명령이 POP한 주소로 이동되는 방식으로 수행되기 때문이다.

 이번에는 if문과 switch문을 어셈블리로 분석해 보도록 하겠습니다.

사실 요새 개강도 있고 학원일도 있고 해서 기존에 올리려던 글들을 못올리고 있네요. 으앙 암튼


1. if 문 분석해 보기

 if 문은 조건부 분기의 일종입니다. 아주 많이 쓰이는 구문이라고 할 수 있지요.

if문 분석에 쓰일 소스는 아래와 같습니다.


#include <stdio.h>


int main()

{

int a = 100;


++a;


if( a < 0 )

{

--a;

}

else if( a == 0 )

{


}

else

{

++a;

}


a = a + 100;


return 0;

}


아주 심플한 소스이지만 어셈블리로는 어떻게 이루어져 있을까요?

소스에 Breakpoint를 걸어놓고 Debug를 클릭하여 디버깅 모드로 들어간 후 Alt+8 혹은 Debug -> Window -> Disassembly 메뉴로 들어가 어셈블리 코드를 보도록 하겠습니다. 디스어셈블리를 하면 아래와 같은 코드를 볼 수 있습니다.

(잡스러운 코드가 길어 if 문만 빼내도록 합니다.)


if( a < 0 )

0041137E  jns         main+3Bh (41138Bh)  

{

--a;

00411380  mov         eax,dword ptr [a]  

00411383  sub         eax,1  

00411386  mov         dword ptr [a],eax  

00411389  jmp         main+4Ch (41139Ch)  

}

else if( a == 0 )

0041138B  cmp         dword ptr [a],0  

0041138F  jne         main+43h (411393h)  

{


}

00411391  jmp         main+4Ch (41139Ch)  

else

{

++a;

00411393  mov         eax,dword ptr [a]  

00411396  add         eax,1  

00411399  mov         dword ptr [a],eax  

}


하나하나 뜯어서 살펴보도록 하겠습니다.


if( a < 0 )

0041137E  jns         main+3Bh (41138Bh)  


if( a < 0 )는 jns main+3Bh와 동일한 문장이라는 의미입니다.

 jns는 무슨 의미일까요? 바로 Jump if the Sign flag is Not set의 줄임말입니다. 플래그 레지스터의 SF가 1로 세팅되어 있지 않다면 뒤에 나오는 라벨 혹은 주소로 분기하라는 의미이죠. SF는 부호 비트 플래그, 음수가 발생하면 일어나는 플래그입니다. 즉 해당 값의 부호비트가 1이 아닐 때(양수일 때) 뒤의 주소로 이동하게 되겠죠. 이를 만족하지 않으면 이 명령어는 없던 일로 취급되어 바로 아래로 내려갑니다.

 jns의 뒤에 있는 main+3Bh (41138Bh)은, jns명령어를 만족할 때 이동하게 되는 주소로, main의 주소(411350h)에서 3Bh만큼 더한 위치(41138Bh)로 이동하겠다는 의미입니다. 이곳은 어디일까요? 바로 두번째 비교(else if( a == 0 ))가 일어나는 곳의 위치로 점프합니다. 처음 if문은 만족하지 못하였으니 else if로 이동하여 두번째 비교를 하라는 의미이죠...


00411389  jmp         main+4Ch (41139Ch)


if문을 수행했다면 더 이상 진행할 필요가 없습니다. 중괄호 밖으로 나오기 위해 jmp합니다..


else if( a == 0 )

0041138B  cmp         dword ptr [a],0  

0041138F  jne         main+43h (411393h)  


이 부분은 원래 의도한 바와는 조금 다르게 되었습니다. ^^;; jnz대신 cmp와 jne가 나왔네요... cmp는 두 값을 비교하는 명령어입니다. 사실 두 값의 차이를 계산하는 명령어라고 할 수 있습니다. 둘 다 같은 값이라면 둘의 차이가 0이 되어 버리므로 ZF가 1로 세팅됩니다. 그리고 상태 플래그를 체크하여 점프하는 jne, je 등의 명령어를 이용해 함께 사용합니다. 이 경우 cmp한 결과가 jne, 즉 not equal시 점프하라는 의미입니다. 즉 if문의 조건에 부합되지 않으면 else문으로 점프시킵니다.


00411391  jmp         main+4Ch (41139Ch)  


역시 이 부분도 else if문을 수행했다면 더 이상 진행할 필요가 없습니다. 중괄호 밖으로 나오기 위해 jmp합니다..

이후 else문은 제일 마지막 부분이므로 점프할 필요도 없고 밖으로 분기할 필요도 없습니다.


이렇게 if문이 이루어 집니다. 어셈블리로 보니 되게 직관적이네요.



2. switch 문 분석해 보기

 이번에는 switch 문을 분석하여 보겠습니다. 우선 분석하기 위한 기본 소스는 아래와 같습니다.


#include <stdio.h>


int main()

{

int a = 1;


switch( a )

{

case 0:

a++;

break;

case 1:

a--;

break;

case 3:

a = 0;

default:

a = 100;

break;

}


return 0;

}


보시는 바와 같이, int형 변수 a에 초기값을 1로 지정하였고, switch문으로 a의 값에 따라 case문에 들어가도록 되어 있습니다.

그리고 case 3을 제외한 모든 경우에 break;를 끝에 넣어주었습니다.


switch( a )

004117C5  mov         eax,dword ptr [a]  

004117C8  mov         dword ptr [ebp-0D0h],eax  

004117CE  cmp         dword ptr [ebp-0D0h],0  

004117D5  je          main+4Bh (4117EBh)  

004117D7  cmp         dword ptr [ebp-0D0h],1  

004117DE  je          main+56h (4117F6h)  

004117E0  cmp         dword ptr [ebp-0D0h],3  

004117E7  je          main+61h (411801h)  

004117E9  jmp         main+68h (411808h)  

{

case 0:

a++;

004117EB  mov         eax,dword ptr [a]  

004117EE  add         eax,1  

004117F1  mov         dword ptr [a],eax  

break;

004117F4  jmp         main+6Fh (41180Fh)  

case 1:

a--;

004117F6  mov         eax,dword ptr [a]  

004117F9  sub         eax,1  

004117FC  mov         dword ptr [a],eax  

break;

004117FF  jmp         main+6Fh (41180Fh)  

case 3:

a = 0;

00411801  mov         dword ptr [a],0  

default:

a = 100;

00411808  mov         dword ptr [a],64h  

break;

}


!?!?!?!?!?!?


다소 복잡한 구조를 띄고 있습니다.

우선 switch문에서는 if문과 같이 값을 비교하여 해당 위치에 맞게 점프해주는 역할을 하는 걸로 보입니다.

케이스 문이 아래에 있을 수록 비교하는 데에 오래걸리겠군요. if 문도 순서에 신경을 써야 하는데 switch문도 순서에 다소 신경을 써야할 것 같아요.


break문은 if문 분석에서 예상했던 바와 같이 switch문을 아예 벗어나는 역할을 담당하고 있습니다.

break문이 없는 case 3:의 경우 해당 점프 문이 아예 없는 것을 볼 수 있습니다.


if문과의 차이점은 if문은 명령어가 시작되는 곳 전으로 점프하여 그곳에서 비교하고 틀리면 점프를 반복하는 반면에,

switch문은 처음부터 모든 경우를 비교하여 점프하는 방식이라는 점과

if문이 해당 조건을 완수하면 다른 조건이 실행되기 전에 밖으로 자동으로 점프하게 해주지만 switch문은 수동으로 점프(break)명령을 넣어주어야 한다는 점인 것 같습니다.

상태 비트의 종류

 이름

 역할

SF(Sign Flag)

  • 결과가 부호 값을 가질 경우(음수일 경우) 발생
  • 발생 예
    • 양수 -> 음수(7Fh->80h)
    • 음수 -> 음수(80h->81h)

CF(Carry Flag)

  • 결과에 올림수나 빌림이 생길 때 발생
  • 발생 예
    • 덧셈에서 올림(Carry) 발생
    • 뺄셈에서 빌림(Borrow) 발생

OF(Overflow Flag)

  • 결과가 한계 값을 초과하는 경우 발생
  • 발생 예
    • 7Fh(양수 최대값)에서 1을 더하여 80h(음수)가 되는 경우
    • 음수 최대값(FFh)에서 1을 더하여 00h(양수)이 되는 경우

ZF(Zero Flag)

  • 결과가 0의 값을 가질 경우 발생
  • 발생 예
    • eax가 1일 때, DEC eax(결과가 0)
    • eax가 -5일 때, ADD eax, 5(결과가 0)



오늘의 명령어

Mnemonic

Operand

역할

INC(Increase)

A

A의 값을 1 증가

DEC(Decrease)

A

A의 값을 1 감소

NEG(Negative)

A

A의 값에 2의 보수를 취하여 값을 반전시킴(양수->음수, 음수->양수)

SHL(Shift Left)

A, B

A의 값을 왼쪽으로 B만큼 쉬프트. (2의 B승 만큼 곱하는 효과) 

SHR(Shift Right)

A, B

A의 값을 오른쪽으로 B만큼 쉬프트. (2의 B승 만큼 나누는 효과) 

MUL(Multiplication)

A, B

A

A와 B를 곱해 eax(Accumulator)에 저장.

피연산자가 한개인 경우 eax의 값에 곱한다.

곱셈은 자릿수가 늘어날 수 있기 때문에 용량이 부족할 수 있어 저장시 1*1=2, 2*2=4 등으로 용량이 확장된다.



연습 문제(Introduction to 80x86 Assembly Language and Computer Architecture pdf 120p)


소스 코드

.386

.MODEL FLAT


ExitProcess PROTO NEAR32 stdcall, dxExitCode:DWORD


INCLUDE io.h


cr    EQU    0dh ; 캐리지 리턴

Lf    EQU    0ah ; 라인 피드


.STACK 4096


.DATA

Prompt1 BYTE "This program will evaluate the expression", cr, Lf, Lf ; 출력할 문장1 시작

BYTE " 2 ( - x + y - 1 ) + z", cr, Lf, Lf

BYTE "for your choice of integer values.", cr, Lf, Lf

BYTE "Enter value for x : ", 0 ; 여기까지 출력됨

Prompt2 BYTE "Enter value for y : ", 0 ; 출력 문장 2

Prompt3 BYTE "Enter value for z : ", 0 ; 출력 문장 3

Value BYTE 16 DUP( ? ) ; 계산 결과 값

Answer BYTE cr, Lf, "The result is " ; 결과 출력 문장 시작

Result BYTE 6 DUP( ? )

BYTE cr, Lf, 0 ; 결과 출력 문장 끝


.CODE

_start:

output Prompt1 ; 문장 1 출력

input Value, 16 ; x값 입력

atoi Value ; ASCII to Integer

neg ax ; 값 반전(-, 2의 보수)

mov dx, ax ; dx에 반전된 x값을 넣음


output Prompt2 ; 문장 2 출력

input Value, 16 ; y값 입력

atoi Value ; ASCII to Integer

add dx, ax ; dx에 y값을 더함( -x+y )


dec dx ; dx에서 1을 뺌( -x+y-1 )


add dx, dx ; dx에 dx의 값을 더함( 2(-x+y-1) )


output Prompt3 ; 문장 3 출력

input Value, 16 ; z값 입력

atoi Value ; z값을 정수형으로 변환

add dx, ax ; dx에 변환된 z값을 더함( 2(-x+y-1))+z )


itoa Result, dx ; 결과 값이 저장된 dx의 값을 ASCII로 바꾸어 Result에 넣음


output Answer ; Answer 문장 출력, 값도 함께 출력됨.


INVOKE   ExitProcess, 0 ; 끝

        

PUBLIC  _start


END



io.h


io.obj


io.h는 어셈블리 컴파일에 필요하고(컴파일만 하여 obj파일로 만들어야함)

io.obi는 링크시 함께 있어야 합니다.



실행 결과

컴파일 : ml /c /coff /Zi **.asm

- /Zi는 디버깅 시 추가

링   크 : link /debug /subsystem:console /entry:** /out:**.exe **.obj ##.obj Kernel32.lib

- /debug는 디버깅 시 추가

C코드를 어셈블리 코드로 보는 법


디버깅 중에, VS2010에서 Debug -> Window -> Disassembly를 실행해 C코드를 어셈블리 코드로 치환한 것을 볼 수 있다.



분석해 보기

1. int a = 100;

0041348E  mov         dword ptr [a],64h

- 0041348E : 어셈블리 명령어가 있는 주소. 바로 다음에 나오는 주소에서 빼면 몇 바이트 명령어인지 알 수 있다.

- mov : 명령어. 대입연산자에 해당한다.

- dword : Double Word, 4Byte 자료형(int)

- ptr [a] : 변수 a의 주소를 나타냄. ptr = pointer, *( &a )에 해당. 결국 a를 의미

- 64h : h는 Hexadecimal을 의미(16진수), 16진수 64는 100이다.

-> o는 Octal(8진수), b는 Binary(2진수)를 의미한다.


=> 결국 a의 메모리 주소에 100의 값을 넣는다고 할 수 있다.


2. a = a + 1;, a++;, ++a;

이 세 코드는 기본적으로 같은 역할을 하지만 코드는 다르다.

그럼 실제로 속도나 코드에서 차이가 있을까?


a = a + 1;

0041349C  mov         eax,dword ptr [a]  

0041349F  add         eax,1  

004134A2  mov         dword ptr [a],eax  

++a;

004134A5  mov         eax,dword ptr [a]  

004134A8  add         eax,1  

004134AB  mov         dword ptr [a],eax  

a++;

004134AE  mov         eax,dword ptr [a]  

004134B1  add         eax,1  

004134B4  mov         dword ptr [a],eax  


자세히 보면 알 수 있지만, 다 같은 코드이다.

속도, 용량, 모든게 다 똑같음을 알 수 있다.(컴파일러마다 다를 수도 있다)

참고로 ++와 --에 해당하는 전용 어셈블리 명령어도 존재한다.

inc(increase)와 dec(decrease)로, add나 sub보다 용량이 적다.(속도면에서는 아마 느린 모양 byte수가 다르므로)


3. a = b + a;

004134B7  mov         eax,dword ptr [b]  

004134BA  add         eax,dword ptr [a]  

004134BD  mov         dword ptr [a],eax  

- eax : 메모리의 값은 메모리가 직접 연산할 수 없으므로, ALU가 사용할 수 있게 임시 레지스터에 전송해야 한다. 그 중 제일 만만한 녀석이 eax이다.


1. 메모리에 있는 4바이트 b값을 연산하기 위해 eax레지스터에 저장한다.

2. eax 레지스터에 있는 값과 메모리에 있는 4바이트인 a의 값을 더한다.

3. 메모리에 있는 4바이트 a 주소에 eax값을 집어넣는다.


4. b = a;

004134C0  mov         eax,dword ptr [a]  

004134C3  mov         dword ptr [b],eax

1. 4바이트 a의 값을 eax 레지스터에 넣는다.

2. eax레지스터에 있는 값을 4바이트 b주소에 넣는다.


=> 즉, a=b;같이 메모리에 있는 값끼리 바꾼다 하더라도 CPU를 무조건 거쳐야 한다.

- 이를 생략하는 것이 DMA(Direct Memory Acces)

+ Recent posts