Win32 어셈블리 프로그래밍 - 2. 메시지 박스

2022. 4. 4. 14:56프로그래밍

728x90

MessageBox

이번에는 윈도우즈 API를 사용해 본다.
메시지 박스는 여러개의 동적 라이브러리 안에 존재한다.  예를 들어 kernel32.dll, user32.dll and gdi32.dll 같은 것들이다.
Kernel32.dll 는 메모리와 프로세스를 조작 할 수 있는 API 함수를 포함하고 있으며, User32.dll 는 사용자 인터페이스 면을 조정한다. Gdi32.dll 는 그래픽 조작 등을 책임진다.

 
윈도우 프로그램은 이런 주요 DLL들을 동적으로 링크하여 구동하게 되는 것이다 하지만 일반적으로 이 DLL 들을 다 포함 할 수 없기 때문에 실행 파일 내에서 이런 DLL이 어디에 포함되어 있는 지 정보 정도는 있어야 한다.

이게 바로 라이브러리를 임포트 하는 이유다(import libraries)


다음으로 API 함수를 분류를 살펴보면 윈도우에서는 두개의 API 함수 분류가 존재 한다.


하나는 ANSI 용 이고 다른것을 UNICODE 용을 위해서 존재한다.

따라서 이 두 API의 분류를 윈도우에서는 다음과 같이 첨자를 붙여 사용하는 데,

ANSI는 "A" 의 첨미를 가진다. 예를 들면 ANSI의 메시지 박스 함수는 MessageBoxA이고, 유니 코드용은 뒤에 "W"를 붙인다.

예제
프로그램의 기본 골격을 만들어 봅시다.

.386
.model flat, stdcall
.data
.code
start:
end start

일반적으로 Win32 프로그램이 해당 프로그램을 끝내야 할 때,

API 함수인 ExitProcess 를 호출하여 종료 하는 데 다음과 같이 어셈블리에서는 적용해 줍니다.

ExitProcess proto uExitCode:DWORD

 

위의 코드는 함수의 프로토타입이라고 말하는 데, 어셈블리가 타입 체크에 필요한 함수의 속성 들을 담고 있는 것을 프로토타입이라고 일컫는다.

프로타입의 형식 선언 :

FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,... 

간단히 말해서, 함수 이름 다음에 PROTO를  붙이고,  콤마로 구분한 파라메터의 데이터 타입의 리스트를 붙이면 됩니다.

위 ExitProcess 함수는 DWORD 타입의 파라메터 하나를 가지는 함수입니다. 

함수의 프로토 타입은 상위레벨 호출 문법을 사용 할 때 유용하다. 더 나아가서는 타입 체크를 위해서 invoke를 호출 할 수 있다.

call ExitProcess

dword 값을 넣지 않고 위의 문장을 실행 한다면, 어셈블러나 링커는 에러를 뱉지는 않겠지만, 이 프로그램을 실행 한다면  낭패를 볼 수 있다

다음 문장으로 대체한다면 

invoke ExitProcess

링커는 DWORD 값을 스택에 밀어넣지 않았다고 알려줄 것이고 나중에 런타임 에러를 피하게 해줄 것입니다.
이렇게 단순히 CALL 함수를 호출 하느니 invoke를 추천합니다.

 

문법은 다음과 같습니다.

INVOKE expression [,arguments]

expression은 함수의 이름이거나 함수의 포인터가 될 수 있다.

함수 파라메터는 콤마로 분리하여 적어준다.
API 함수의 대부분의 함수 프로토타입들은 인클루드 파일안에 존재한다.

만약 hutch's MASM32를 사용한다면 MASM32/include 폴더 안에 있을 것이다.

인클루트 파일은 .inc 확장자를 가지고 DLL 내의 함수들의 함수 프로토타입들은 DLL과 같은 이름의 .inc 파일내에 저장하여 가진다.

예를 들면, ExitProcess는 kernel32.lib에 있기 때문에 ExitProcess 함수의 함수 프로토타입은 kernel32.inc 파일에 저장되어 있다. 또 자신만의 함수에 대한 함수프로토타입을 생성할 수도 있다.


이 예제에 사용한것은 http://win32asm.cjb.net 에서 다운로드 받을 수 있는 
hutch의 windows.inc 파일이다.

다시 ExitProcess 함수로 돌아가서 uExitCode 파라메터는 사용자가 정하는 프로그램이 종료된 후에 프로그램이 윈도우로 리턴하기 원하는 값이다. 다음과 같이 ExitProcess를 호출 할 수 있는 데 0은 말그대로 0을 리턴하고 싶다는 것이다.

invoke ExitProcess, 0

아래의 코드 중에 start 라벨 바로 아래에다가 적어주면, 윈도우가 즉시 사라지는 윈32 프로그램을 가지게 될 것이다. 그럼에도 불구하고, 이것도 유효한 프로그램이다.

.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib                                                                  
.data
.code
start:
invoke ExitProcess,0
end start

 

option casemap:none :

이 선언은 MASM으로 하여금 라벨들이 대소문자 구별하도록 해주어, ExitProcess 와 exitprocess 는 두개의 의미가 틀리게 만들어 준다.

새로운 지시어인 include에 주의해 보자
이 지시자는 넣고 싶은 라이브러리를 담고 있는 파일 이름을 적어주면 된다.

 

위의 예제에서 include \masm32\include\windows.inc 라인의 의미는 \MASM32\include 내에 있는 windows.inc 파일을 열어 줄것이고

windows.inc 파일 내에 있는 내용을 복사한 것처럼 windows.inc 내용을 프로세싱 할 것이다. 

hutch's  windows.inc 파일 내에는 윈32 프로그램을 하는 데 필요한 구조와 상수의 정의들을 담고 있다.

 

여기에는 어떤 함수 프로토타입도 없다. 그리고 windows.inc 파일은 포괄적인 내용을 담고 있다고는 할 수 없다.

hutch와 나는 가능한한 많은 수의 상수와 구조체를 담을려고 했으나 역시 더 많은 것들이 include 되어야만 한다. 지속적으로 업데이트 할 것이다.

업데이트가 있는 지 여부를 내 홈페이지에서 항상 확인하기 바란다.
windows.inc 파일에서 이 프로그램은 상수와 구조체 정의를 얻을 것이다. 그리고 함수 프로토타입인데, 다른 인클루드 파일들이 필요하다. 
이 파일들은 모드  \masm32\include 폴더 내에 존재한다.


위의 예제의 경우에는 kernel32.dll에서 익스포트한 함수를 호출하기 때문에 kernel32.dll에서 함수 프로토타입을 인클루드 하는 것이 필요하다. 그 파일은 바로  kernel32.inc 만약 이 파일은 텍스트 에디터에서 연다면, kernel32.dll 파일 내에 들어 있는 모든 함수 프로토타입을 볼 수 있을 것이다. kernel32.inc를 인클루드 하지 않으면 어떻게 될까?

 

단순히 call 문법을 사용해서 ExitProcess를 호출 할 수 있을 것이다. 하지만 invoke 함수는 호출 하지 못한다. 여기서 주요한 점은 함수를 호출하기 위해서는 소스코드 내에 어딘가에 함수의 프로토타입을 집어넣어야만 한다는 것이다. 위의 예제에서 kernel32.inc를 인클루드 하지 않으면, ExitProcess 함수에 대한 함수프로토 타입을 소스코드 위에 어딘가에 정의하게 되면 아무일 없이 잘 돌아 갈것이다.

 

즉 인클루드 파일은 내가 원할때마다 함수의 프로토타입을 일일이 치지 않고 재사용 할 수 있게 만들어 준다는 것이다.
다음으로 또다른 새로운 지시자를 만나는 데, includelib가 그것이다.  includelib는 include 같은 행동양식을 가지지 않는다

이는 내 프로그램이 사용하는 라이브러리가 무엇인지 어셈블러에게 알려주는 역할을 한다.

 

어셈블러가 includelib 지시자를 만나면 내 프로그램이 무엇을 링크 시키기는 원하는 지를 링커가 알 수 있도록 오브젝트 내에 링커 명령을 넣어준다. includelib 지시지를 사용하도록 강제하지는 않는다.

이를 대신해서 링커의 커맨드라인에다 라이브러리 이름을 전부 써주는 방법도 있다 하지만 이 방법은 정말 지겨울 뿐 아니라 명령어 라인은 단지 128 문자만 들어 갈 수 있다는 것이 걸림돌이 될 것이다.

 

그럼 위의 프로그램을 msgbox.asm 이라고 부르고, ml.exe 가 경로에 잡혀 있다고 가정하고 msgbox.asm 파일을 다음과 같이 어셈블 해보면:


ml  /c  /coff  /Cp msgbox.asm

/c 
어셈블만 하겠다고 MASM에 알림. link.exe를 호출하지 마라. 대부분 link.exe를 호출하기 앞서 다른 몇몇 태스크가 실행되어야 할 것이라면 link.exe가 자동으로 호출되길 원하지 않을 수도 있다.

/coff 
MASM으로 하여금 COFF 형식.obj 파일을 생성하라고 명령한다. MASM은 유닉스 상에서 생성한 오브젝트와 실행 파일 포맷으로 사용되는 COFF (Common Object File Format) 변종 형태의 포맷을 사용한다.

/Cp
MASM이 사용자 ID의 대소문자를 보존하라는 명령이다. hutch's MASM32 패키지를 사용한다면 소스코드 머릿부분이나 .model 바로 밑에다  "option casemap:none" 를 집어 넣기만 하면 이런 효과가 생기게 된다.

 

msgbox.asm 파일을 성공적으로 어셈블 하게 되면, msgbox.obj 파일을 갖게 된다. msgbox.obj 파일은 오브젝트 파일이다. 이 오브젝트 파일은 실행파일을 만들기 위한 단지 한 단계 과정에 불과하다. 이 파일은 인스트럭션/데이터를 바이너리 형식으로 가진다.

 

부족한 부분은 링커에 의해서 주소들이 약간씩 고쳐져야 한다.
그리고 link 명령을 구동하자:

link /SUBSYSTEM:WINDOWS  /LIBPATH:c:\masm32\lib  msgbox.obj

/SUBSYSTEM:WINDOWS 
이 실행 파일이 어떤종류인지를 알린다.

/LIBPATH:<path to import library>
임포트 라이브러리가 어딨는 지 알린다. MASM32 사용한다면, MASM32\lib 폴더에 있을 것이다.


링커는 오브젝트 파일을 읽어서 임포트 라이브러리로 부터 주소를 읽어 현재 주소를 고쳐줄 것이다. 프로세싱이 끝나면 msgbox.exe 파일이 생성된다.
msgbox.exe 파일을 얻었다면 계속해서 실행해보자. 아무것도 아니다. 그치만, 아직 흥미로운 내용을 여전히 많이 다루지는 않았다. 그럼에도 불구하고 이 프로그램은 윈도우 프로그램이다.
그리고 크기를 함 보자 내 피씨에서는 1,536 바이트이다.

다음으로 메시지 박스를 넣어볼 차례다.

 

이 함수 프로토타입은 다음과 같습니다:

MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD

hwnd
부모 윈도우의 핸들이다. 핸들이란 윈도우가 참고하고 있는 숫자라고 생각하면 된다. 그 값은 우리에게 그닥 중요하지 않다. 단지 기억할 것은 윈도우를 나타낸다라고 생각해 보면 된다. 윈도우에 뭔가를 하고 싶다면 이 핸들을 참조해야만 할 것이다.

lpText
메시지박스의 클라이언트 영역에 표시할 텍스트의 포인터이다. 포인터는 무언가의 주소를 나타낸다. 텍스트 문자열의 포인터 == 텍스트 문자열의 주소 이다.


lpCaption
메시지 박스의 캡션에 대한 포인터


uType
아이콘과 메시지 박스상의 버튼들의 타입과 숫자을 명시한다.


메시지 박스를 인클루드 하기 위해서 msgbox.asm를 변경하자.

.386
.model flat,stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
includelib \masm32\lib\kernel32.lib
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
.data
MsgBoxCaption db "Iczelion Tutorial No.2",0
MsgBoxText db "Win32 Assembly is Great!",0
.code
start:
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
invoke ExitProcess, NULL
end start

 

어셈블하고 구동 해보자. "Win32 Assembly is Great!" 라는 텍스트가 메시지 박스에 표출되는 것을 볼 수 있다.
 
다시 소스코드를 들여다 보자.


우리는 두개의 zero-terminated 문자열을 .data 섹션에서 정의했다.

윈도우 안의 모든 ANSI 문자들은 NULL (0 hexadecimal)로 끝나야만 한다.

우리는 NULL 과 MB_OK 두개의 상수를 사용했다. 이 상수들은 windows.inc 파일 내에 설명되어 있으며, 그 값들 대신에 이름으로 이 값들을 참조 할 수 있는 것이다.

이렇게 함으로써 소스코드의 가독성을 높일 수 있습니다.


addr 오퍼레이터는 함수의 라벨의 주소를 넘기는 데 사용한다. invoke 지시자의 컨텍스트 내에서만 유효하다.

레지스터/변수의 라벨의 주소에 할당하지 못한다는 것이다. 예를 들어 위의 예제의 addr 대신에 offset를 사용할 수는 있다.

 

하지만 이 둘 사이에는 다음과 같은 차이가 있습니다:
addr 은 offset이 할 수 있는 forward reference를 못합니다.

예를 들면 invoke 라인이 아니라 소스코드 내의 어떤곳에 라벨이 정의되어 있으면 addr은 제대로 작동하지 않을 것이다. 

invoke MessageBox,NULL, addr ​

 

invoke MessageBox,NULL, addr ​


MsgBoxText,addr MsgBoxCaption,MB_OK 
...... 
MsgBoxCaption  db "Iczelion 
Tutorial No.2",0 
MsgBoxText       db "Win32 Assembly is Great!",0

 

위의 예제는 에러를 생성하는 데, 위의 조각코드 내에 addr 대신에 offset을 사용한다면 MASM은 위 코드를 어셈블 했을 것이다.


addr은 offset이 못하는 로컬 변수를 조작할 수 있다. 로컬 변수는 스택의 어디 에약된 공간이다.

런타임 시에만 이 주소를 알 수 있는 데, 

offst은 어셈블러가 어셈블 하는 동안 해석되어버리기 때문에 이를 알 수가 없다. 로컬 변수를 조작하는 것이 불가능한게 당연하다. 만약 전역변수라면 오브젝트파일내에 그 변수의 주소를 넣는다.  이런점에서 offset으로 동작한다.

만약 로컬변수라면 함수에서 실제적으로 불려지기전에 인스트럭션 시퀀스를 생성한다:

lea eax, LocalVar
push eax

 

lea는 런타임시에 라벨의 주소를 정할 수 있기 때문에 제대로 동작 할 것이다.

​이상.

728x90