communication with C
고급언어(C)에서 어셈블리어를 사용는데 2가지 방법이 있다.
C에서 어셈블리 서브루틴을 호출하던지, 인라인(inline) 어셈블리를 사용하는 것이다.
인라인 어셈블리는 프로그래머로 하여금 C 코드에 어셈블리 문장을 직접적으로 넣을 수 있게 해주는 것이다.
단 인라인 어셈블리를 사용시 반드시 컴파일러가 사용하는 형식으로 코드를 작성하여야 한다.
현재 NASM을 지원하는 컴파일러는 없다.
어셈블리 루틴을 사용하는 이유
- C로 하기에 매우 어렵거나 매우 힘든 컴퓨터 하드웨어의 직접적인 접근
- 매우 빠르게 작동 되어야만 루틴, 컴파일러가 할 수 있는 것보다도 빠르게 프로그래머가 직접적으로
최적화 해야 하는 부분(최근은 컴파일러가 매우 최적화 되어 있어 별로 차이가 없다.)
C 호출 규약
앞에서 설명한 것 외의 C 호출 규약들..
레지스터에 저장하기
C는 서브루틴이 다음의 레지스터에 값을 보관한다고 생각한다.: EBX, ESI, EDI, EBP, CS, DS, SS, ES
위의 레지스터의 값은 변경은 가능하지만, 서브루틴이 리턴할 경우 이전 값들이 복원가능하여야 한다.
EBX, ESI, EDI는 절대 변경되어서는 안된다. C에서는 이 레지스터들을 레지스터 변수(register variable)로 사용한다.
함수들의 라벨
대부분의 C컴파일러들은 함수나, 전역/정적 변수들의 이름 앞에 _ 한개를 붙인다.
ex) f라는 이름의 함수는 _f라는 이름의 라벨로 대응된다.
리눅스 gcc 컴파일러의 경우 어떠한 문자도 붙지 않는다.
리눅스 ELF 실행가능 파일들의 경우 C함수 f에 대하여 그냥 f라는 이름의 라벨을 사용한다.
DJGPP의 gcc는 _를 붙인다.
인자 전달하기
위의 C코드는
segment .data
x dd 0
format db "x=%d\n",0
segment .text
// ...
push dword [x]
push dword format
call _printf
add esp,8
printf는 C 라이브러리의 함수 중 하나로, 가변 개수의 인자를 가질 수 있다.
x의 값(ebp+12) |
형식 문자열의 주소(ebp+8) |
주소를 리턴(ebp+4) |
EBP를 저장(ebp) |
위의 어셈블리 코드를 통해 printf를 호출하였을 때 스택은 위와 같은 형태가 될 것이다.
위와 같이 형식 문자열의 주소가 항상 'EBP+8’에 저장 될 것이다.
printf 코드는 형식 문자열을 통하여서 얼마나 많은 수의 인자들이 전해졌는지 알아내어
스택에서 찾아 볼 수 있다.
(stdarg.h 헤더파일은 위 과정을 손쉽게 할 수 있는 메크로를 제공하여 준다.)
printf(“x=%d\n”)와 같은 실수를 하게 된다면,
printf 코드는 [EBP+12]에 위치한 더블워드 값을 출력할 것이다.
지역 변수의 주소 계산하기
data나 bss 세그먼트의 라벨의 주소는 링커가 간단하게 계산하여 준다.
하지만 직관적으로 스택에 저장된 지역 변수(인자)의 주소를 계산하는 것은 어렵다.
위의 코드는 사용할 수 없다.
MOV 명령을 통해 EAX로 저장되는 값이 계산되어야만 하기 때문이다.(피연산자가 반드시 상수여야 한다.)
LEA(Load Effective Address)
위와 같은 연산을 위한 명령어이다.
위 코드는 'ebp-8’의 주소를 가지고 eax에 전달하여 준다.
( [ebp-8]이 'ebp-8’의 값을 가르키는 것이 아니다. LEA 명령은 절대로 메모리를 읽어드리지 않는다.
오직 다른 명령이 읽어들일 주소값을 계산하고, 이를 첫 번째 레지스터 피연산자에 저장할 뿐이다.
따라서 별도로 메모리 크기를 지정할 필요가 없다. )
리턴값
void C 함수가 아닌 함수들은 모두 값을 반환한다.
C 호출 규약에서는 리턴값들은 레지스터를 통하여서 전달된다고 정하였다.
모든 정수형(char, int, enum, etc…)리턴값들은 EAX 레지스터에 저장되어 리턴된다.
만일 리턴값이 32비트보다 작을 경우 확장되어 EAX에 저장된다.
64비트의 값들은 EDX:EAX 레지스터 쌍에 저장된다.
부동 소수점 값들의 경우, 수치 부프로세서의 ST0 레지스터에 저장된다.
C 이외의 언어와 어셈블리가 같이 사용되는 경우가 많다.
많은 경우 기본적으로 표준 호출 규약을 이용한다.(모든 컴파일러가 그런것은 아님)
여러 호출 규약을 이용할 수 있는 컴파일러의 경우,
커멘드 라인에서 스위치를 이용하여 기본값으로 어떤 규약을 이용할지 선택할 수 있다.
또한 C의 문법을 확장하여 개개의 함수를 다른 호출 규약을 이용하여 컴파일 할 수 있다.
단 확장이 표준화 되어 있지 않으며, 컴파일러 마다 다를 수 있다.
GCC 컴파일러는 여러 종류의 호출 규약을 지원한다.
함수의 호출 규약은 __attribute__. 확장을 이용하여 개별로 지정할 수 있다.
ex) f란 이름의 1개의 int 인자를 가지는 void 함수
void f(int) __attribute__((cdecl));
GCC는 또한 표준 호출(standard call) 규약을 지원한다.
stdcall로 정의 할려면 위의 코드의 cdecl를 stdcall로 바꾸면 된다.
stdcall과 cdecl의 차이점은
stdcall의 경우 서브루틴이 스택으로부터 인자를 제거해야 된다는 것이다.
따라서 stdcall 호출규약은 오직 고정된 수의 인자를 가지는 함수에게서만 사용될 수 있다.
또한 GCC는 regparm라는 attribute를 지원하여 컴파일러로 하여금 스택을 이용하지 않고
레지스터를 통해 최대 3개의 정수 인자를 전달하게 한다.
볼랜드와 마이크로소프트는 호출 규약을 선언할 때 동일한 문법을 사용한다.
C에 __cdecl와 __stdcall 키워드를 추가하여 사용한다.
cdecl & stdcall
cdecl
cdecl는 매우 단순하고 다루기 쉽다. 또한 어떠한 형식의 C함수나 C 컴파일러에서든지 사용할 수 있다.
하지만 다른 호출 규약들에 비해 느릴 수 있고, 더 많은 메모리를 차지한다.
(함수가 호출 될 때마다, 코드에 저장된 인자들을 제거하는 명령을 수행해야 된기 때문에)
stdcall
stdcall은 cdecl보다 메모리를 적게 소모한다. CALL 명령 이후에 스택을 정리할 필요가 없다.
하지만 가변 개수의 인자들을 가지는 함수들에게 사용될 수가 없다.
regparm
regparm은 레지스터를 이용하는 호출 규약임으로 인자를 전달하는 속도가 매우 빠른다.
하지만 이러한 규약들은 보다 더 복잡하다.
인자들 중 일부는 레지스터에 나머지는 스택에 저장되어야하기 때문이다.
main5.c-2