만약 여러분이 C 와 어셈블리가 혼합된 프로젝트를 수행하고 있다면, 지금 소개하고자 하는 방법을 추천하고자 한다. 리눅스 커널에서 .S 의 확장자를 가진 파일들 중에서, gas 가 처리하는 (as86 이 처리하는 파일이 아님!) 파일들을 통해서 유용한 예제를 발견할 수 있다.
32비트의 argument 들은 스택에 푸시될 때, 32비트의 근거리 반환 주소 (32-bit near return address) 의 윗부분에 역순으로 푸시되어진다. 따라서, 그것들이 pop 될 때에는 올바른 순서로 pop 되어진다. %ebp, %esi, %edi, %ebx 레지스터들은 피호출자가 저장하게 되고, 다른 레지스터들은 호출자가 저장하게 된다. %eax 는 결과가 32비트 일때 결과를 저장하는 용도로 사용되어지고, 만약, 결과가 64비트라면, 그때는 %edx:%eax 레지스터 쌍이 결과를 저장하는데 사용된다.
FP stack 에 대해서는 확실해 잘 모르겠지만, 저자는 결과가 st(0) 에 저장되고, 스택 전체가 호출자가 저장하는 용도로 쓰인다고 생각한다.
GCC 가 레지스터들의 용도를 미리 지정해서 위에서 설명한 호출 규칙을 수정하는 옵션을 가지고 있다는 것을 잊지 않도록 하라. 자세한 내용은 i386 .info 페이지를 참조하라.
여러분은 GCC 의 표준 함수 호출 규칙을 따르는 함수를 위해서 cdecl 이나 regparm(0) attribute 를 선언해야만 한다는 것을 기억하라. GCC info 페이지의 C Extensions::Extended Asm:: 섹션을 참조하라. 또한, Linux 가 asmlinkage 매크로를 어떻게 정의하고 있는지도 참조하라.
몇몇 C 컴파일러들은 밑줄 문자를 다른 심볼들 보다 먼저 확장한다. 다른 컴파일러는 그렇게 하지 않지만 (역자 주 : 원문의 내용은 Some C compilers prepend an underscore before every symbol, while others do not. 입니다)
Linux 의 a.out GCC 는 그러한 확장을 한다. 그렇지만, ELF GCC 는 하지 않는다.
여러분이 그러한 특성을 극복할 필요가 있다면, 현존하는 패키지들은 어떻게 하는지를 참조하라. 예를 들어, 오래된 리눅스의 소스트리나, 혹은 Elk, qthreads, OCaml 등과 같은 패키지를 구해서 살펴볼 수 있을 것이다.
여러분은 또한, 다음과 같은 문장을 삽입함으로써 함축된(implicit) C -> asm 의 renaming 규칙을 override 할 수도 있을 것이다.
void foo asm("bar") (void); |
binutils 패키지의 objcopy 유틸리티가 여러분의 a.out 오브젝트를 ELF 형식의 오브젝트 파일로 변환해 줄 수 있음을 주목하라. 그리고, 그 반대의 일도 가능할 것이다. 보다 일반적으로 이야기하자면, 그 유틸리티는 더 많은 형태의 파일 형식 변환도 해 줄 것이다.
여러분은 종종 C library (libc) 를 사용하는 것이 유일한 방법이며, 직접적인 시스템 콜은 별로 좋지 않다는 말을 들을 것이다. 이것은 사실이다. 그러나 일반적으로 여러분은 libc 가 신성시되고 있지 않다는 것과, 대부분의 경우 libc 는 몇가지 체크만 하고, 커널을 호출한 후, errno 변수를세팅할 뿐이라는 것을 알아야 한다. 여러분은 여러분의 프로그램에서도 이와 같은 것을 쉽게 할 수 있다 (만약 여러분이 그것을 필요로 한다면). 그리고 여러분이 공유 라이브러리를 사용하지 않는다는 이유만으로도 여러분의 프로그램은 몇십배 더 크기가 줄어들 것이며, 수행성능 또한 더 좋아질 것이다. libc 을 어셈블리 프로그래밍에서 사용하거나 하지 않거나 하는 문제는 실용적인 문제라기 보다는 기호나 신념에 관한 문제이다. Linux 는 POSIX 호환을 목표로 하고 있다는 것을 기억하라. 그리고, libc 는 그러한 목표를 만족한다. 이것은 libc system call 의 거의 모든 문법이 커널의 시스템 콜의 실제 문법과 정확히 일치한다는 뜻이다. 그러나 GNU libc(glibc) 는 버젼업이 되어 갈수록 더 느려지고 있다. 또한 메모리도 더 많이 잡아먹어 가고 있다. 그래서 직접적인 시스템 콜이 꽤나 실용적인 것이 되어가고 있다. 그렇지만... libc 를 사용하지 않음으로써 생기는 주된 부작용(?, drawback) 은 아마도 여러분이 몇몇의 libc 의 특정 함수들(시스템 콜 wrapper 가 아닌)을 직접 구현할 필요가 생길지도 모른다는 것이다. (이를테면, printf() 과 같은...) 그리고, 여러분이 libc 를 사용하지 않기로 결정했다면, 당연히 여러분은 그렇게 할 각오(?)가 되어 있는 것이다. 그렇지 않은가?
여기에 직접적인 시스템 콜을 하는 것의 득과 실이 나열되어 있다 :
이득:
가능한한 코드의 크기를 줄일 수 있다
가능한 최대의 속도를 낼 수 있다.
여러분의 프로그램이나 라이브러리를 여러분이 사용하는 특정 언어나 메모리 요구사항 등 어떤 것에도 적용할 수 있다. 즉, full control 을 얻을 수 있다.
libc cruft 에 의한 공해가 없다(? 원문 : no pollution by libc cruft)
C 의 함수 호출 규칙으로 인한 공해가 없다 (원문 : no pollution by C calling conventions) (만약 여러분이 여러분 자신의 언어나 혹은 환경을 개발하고 있다면)
정적 바이너리들은 여러분을 libc 업그레이드나 크래쉬와 무관하게 해 줄 것이다. 혹은, 쉘의 #! 인터프리터 패스와도 무관하게 해 줄 것이다.
원문 : static binaries make you independent from libc upgrades or crashes, or from dangling #! path to an interpreter (and are faster)
재미있다. (원문 : just for the fun out of it (don't you get a kick out of assembly programming?)
손해:
만약 여러분의 컴퓨터의 다른 프로그램 중 libc 를 사용하는 프로그램이 있다면, libc 의 코드를 두개씩이나 가지는 일은 여러분의 메모리를 낭비할 뿐이다.
여러 정적 바이너리에서 불필요하게 구현된 서비스들 역시 메모리의 낭비일 따름이다. 그러나 여러분읜 여러분의 libc 대체품을 만들어서 공유 라이브러리로 사용할 수 있다.
모든것을 어셈블리로 만드는 것 대신, 바이트코드나 워드코드 혹은, structure interpreter 와 같은 것을 사용함으로써 코드의 크기가 괄목할 만큼 줄어들 것이다. (인터프리터 자체는 C 로 만들든, 어셈블리로 만들든 상관 없다.) 여러개의 바이너리들을 작게 유지하는 최상의 방법은, 여러개의 바이너리 파일을 유지하는 것이 아니라, #! 등과 같은 쉘 인터프리터를 사용해서 인터프리터 프로세스를 하나 마련하는 것이다. 이러한 방법으로 OCaml 은 워드코드 모드 (최적화된 native code mode 와 반대말) 에서 동작한다. 또한, 그렇게 하는 것은 libc 를 사용하는 것과도 호환성이 있다. 이 방법은 Tom Christiansen 이 Perl PowerTools 에서 유닉스 유틸리티들을 새로 구현한 방법이다. 마지막으로, 하드코드된 경로에 의한 외부 파일에 의존하지 않고서 코드의 크기를 줄이는 방법으로, 단지 하나의 바이너리 파일만 유지하고, 그 파일에 대한 여러가지 이름의 hard 혹은 soft 링크를 유지하는 방법이 있다 : 동일한 바이너리 파일은 최적의 공간에서 여러분이 원하는 모든것을 제공할 것이며, 그것에는 불필요한 서브루틴이나 혹은 필요없는 바이너리 헤더도 없을 것이다. 바이너리는 argv[0] 에 의해서만 행동을 결정할 것이다. 특정 이름으로 호출되지 않은 경우에는 디폴트로 쉘이 될 수도 있으며, 아마도 인터프리터로 동작하지 않을수도 있다.
(원문을 함께 개제합니다 : Finally, one last way to keep things small, that doesn't depend on an external file with a hardcoded path, be it library or interpreter, is to have only one binary, and have multiply-named hard or soft links to it: the same binary will provide everything you need in an optimal space, with no redundancy of subroutines or useless binary headers; it will dispatch its specific behavior according to its argv[0]; in case it isn't called with a recognized name, it might default to a shell, and be possibly thus also usable as an interpreter!
여러분은 리눅스 시스템 콜 이외에도 libc 가 제공하는 많은 기능들을 유용하게 사용할 수 없다 : 즉, 맨페이지 섹션 3 에 나오는 여러가지 기능들, 이를테면, malloc, threads, locale, password, high-level network management 등과 같은 것을 사용할 수 없다는 이야기이다.
따라서, 여러분은 libc 의 대부분을 새로 구현해야 할지도 모르다. 즉, printf() 로부터 malloc() 과 gethostbyname 까지도 직접 구현해야 할 것이다. 그렇게 하는 것은 불필요한 일이며, 때때로 매우 지루한 작업이 될 수도 있다. 몇몇 사람들이 벌써 "light" 버젼의 libc 를 구현해 두었다는 것에 주목하고, 그러한 작업의 결과물을 체크해 보도록 하라. (레드햇의 minilibc : Rick Hohensee's libsys, Felix von Leitner's dietlibc, Christian Fowelin's libASM, asmutils 이 프로젝트는 순수 어셈블리 libc 를 가지고 작업하고 있다.)
정적 라이브러리를 사용하면 여러분은 libc 업그레이드나 zlibc 패키지 (gzip 으로 압축된 파일들에 대해서 투명한 압축풀기를 수행해 주는) 와 같은 libc add-on 들로부터 이득을 얻을 수 없게 됩니다.
libc 에 의해서 추가된 몇몇 인스트럭션은 시스템 콜과 비교해서 믿기지 않을만큼 적은 속도의 오버헤드를 가진다. 만약 수행속도가 중요한 문제라면, 여러분의 주된 문제는 여러분이 시스템 콜을 사용하는데 있을 것이며, 여러분이 사용하는 wrapper 가 구현된 방식에는 문제가 없을 것이다.
L4Linux 와 같은 micro-kernel 리눅스 버젼을 사용할 때에는 시스템 콜의 표준 어셈블리 API 를 사용하는 것이 libc 의 API 를 사용하는 것 보다 훨씬 느릴 수 있다. 그러한 마이크로 커널 리눅스 버젼은 자신만의 보다 빠른 호출 규칙을 가지고 있으며, 표준 호출 규칙을 사용하는 경우 매우 높은 convention translation (호출 규칙 변환) 오버헤드가 걸릴 수 있다. (L4Linux 는 그에 맞는 syscall API 를 이용해서 재컴파일된 libc 와 함께 제공된다 : 물론, 여러분은 여러분의 코드를 제공된 API 를 이용해서 다시 컴파일 해야 할지도 모른다)
앞서 다루었던 일반적인 속도 최적화에 대한 토의를 참조하라.
만약에 시스템 콜이 여러분이 보기에 너무 느려 보인다면, 여러분은 userland 에 머물기보다는 커널 소스를 직접 해킹해서 원하는 결과를 얻을 수 있다.
만약 여러분이 방금 언급한 득실들을 심사숙고 했으며, 그래도 여전히 direct 시스템 콜을 사용하기를 원한다면, 아래에 몇가지 조언을 첨부해 주겠다 :
여러분은 여러분의 시스템 콜 함수들을 asm/unistd.h 을 include 하고, 제공된 매크로들을 사용함으로써 C 로 포터블하게(어셈블리를 사용한 포터블하지 않은 형태와 반대되는) 구현할 수 있다.
여러분이 그것을 대체하려고 하기 때문에, libc 소스 코드를 구해서, 그것을 해킹해 보라 (and grok them) (그리고, 만약 여러분이 더 잘 할 수 있다고 생각한다면, libc 저자들에게 피드백을 할 수 있도록 하라!)
여러분이 원하는 모든것을 해 주는 순수 어셈블리 코드의 예제로써, 이 문서의 뒷부분에 나오는 리소스 목록을 참조하라.Linux assembly resources.
기본적으로, 시스템 콜은 int 0x80 인스트럭션을 __NR_ 시스템 콜 번호 를 (asm/unistd.h 파일로부터.) eax 레지스터에 넣고, 파라미터들 (six개 까지) 은 각각 ebx, ecx, edx, esi, edi, ebp 레지스터에 넣어서 호출한다.
리턴값은 eax 레지스터에 담겨지게 되며, 음수의 리턴값은 에러가 있음을 나타낸다. libc 가 errno 변수에 집어넣는 값과 대응되는 값이다. 사용자 영역의 스택은 영향을 받지 않는다. 그래서 여러분은 시스템 콜을 행할 동안에 유효한 것을 가질 필요는 없다(?)
원문 : Result is returned in eax, negative result being an error, whose opposite is what libc would put into errno. The user-stack is not touched, so you needn't have a valid one when doing a syscall.
참고: ebp 레지스터에 여섯번째 파라미터를 담아서 넘겨주는 것은 Linux 2.4 에서부터 볼 수 있다. 그 이전의 리눅스 버젼들은 레지스터에 담기는 다섯개의 파라미터만 인식한다.
Linux Kernel Internals, 문서의 How System Calls Are Implemented on i386 Architecture? 장은 여러분에게 보다 확실한 개념을 제공할 것이다.
프로세스를 처음 시작할 때 넘겨지는 인자들에 대해서는, 일반적인 원칙은 스택이 인자의 갯수를 저장하는 argc 의 값과, *argc 를 구성하는 포인터의 리스트와 NULL-terminated 되는 variable=value 의 환경변수 (environ) 값들의 리스트를 저장한다는 것이다.
원문 : As for the invocation arguments passed to a process upon startup, the general principle is that the stack originally contains the number of arguments argc, then the list of pointers that constitute *argv, then a null-terminated sequence of null-terminated variable=value strings for the environment.
보다 자세한 정보를 원한다면, 리소스 리스트를 참조해서 (: Linux assembly resources) libc 의 C 스타트업 코드의 소스를 읽어 보아라. (crt0.S or crt1.S) 혹은 리눅스 커널의 소스를 참조하라 . (exec.c and binfmt_*.c in linux/fs/).
여러분이 리눅스에서 직접 I/O 포트에의 입출력을 수행하고자 한다면, OS 의 중재를 필요로 하지 않는 매우 간단한 일이어서 IO-Port-Programming 미니 하우투를 참조하면 될 정도이거나 아니면 커널 디바이스 드라이버를 필요로 해서 여러분이 커널 해킹이나 디바이스 드라이버 개발이나, 커널 모듈과 같은 것들에 대해서 더 많은 것을 습득해야만 하는 경우이거나 둘중 하나의 경우일 것이다. (커널 모듈이나 디바이스 드라이버에 대한 것들은 LDP 에 매우 훌륭한 하우투들이 존재한다.)
혹시 여러분이 원하는 것이 그래픽 프로그래밍인 경우, 다음의 프로젝트들을 참조하라 : GGI 혹은 XFree86 프로젝트
어떤 사람들은 작고, 견고한 XFree86 드라이버들을 interperted domain-specific language 인 GAL, 로 개발함으로써 보다 효과적인 결과를 얻었다.
원문 : Some people have even done better, writing small and robust XFree86 drivers in an interpreted domain-specific language, GAL, and achieving the efficiency of hand C-written drivers through partial evaluation (drivers not only not in asm, but not even in C!). The problem is that the partial evaluator they used to achieve efficiency is not free software. Any taker for a replacement?
어쨌거나, 이 모든 경우에도, 여러분은 linux/asm/*.h 에 있는 GCC 인라인 어셈블리와 매크로를 사용함으로써 전체를 어셈블리로 코딩하는 것보다 효율적으로 일을 수행할 수 있다.
그러한 일들은 이론적으로 가능하다 (증명 : DOSEMU 가 프로그램들에게 선택적으로 하드웨어 포트 접근 권한을 주는 방법을 보라) 그리고, 저자는 어딘가의 누군가가 실제로 그러한 일을 했다는 소문을 들었다. (PCI 드라이버에서인가? 아니면, VESA 드라이버에서인가... 음... ISA PnP 였던가... 잘 모르겠다) 만약 여러분이 보다 정확한 정보를 가지고 있다면 환영한다. (역자주 : 메일을 달라는 뜻인듯) 어쨌든, 정보를 찾기 매우 좋은 곳은 리눅스의 커널 소스와 DOSEMU 의 소스이다. (그리고, DOSEMU repository 의 다른 많은 프로그램들이다.) 또한, 여러가지의 리눅스의 low-level 프로그램들의 소스들도 도움이 될 것이다. (perhaps GGI if it supports VESA)
기본적으로 여러분은 16비트 보호모드나 혹은 vm86 모드를 이용해야만 할 것이다.
전자(16비트 보호모드) 는 셋업은 비교적 쉬우나 세그먼트 조작이나 절대 세그먼트 주소지정(segment 0 을 주소지정하는 것과같은) 이 없는 잘 동작하는 코드를 만들어야만 동작한다. 우연이라도 모든 사용하는 세그먼트가 LDT(역자주 : Local Descriptor Table, 인텔 아키텍쳐에서 세그먼트와 논리 주소, 물리 주소들을 변환하는 데 사용되는 자료구조) 를 마리 셋업해 버리지 않는 한은 그러하다
원문 : The first is simpler to setup, but only works with well-behaved code that won't do any kind of segment arithmetics or absolute segment addressing (particularly addressing segment 0), unless by chance it happens that all segments used can be setup in advance in the LDT.
후자(vm86 모드) 는 'vanilla 16-bit 환경' 에서 보다 많은 호환성을 제공하기는 하지만, 더 복잡한 핸들링이 필요하다.
두 경우 모두 여러분이 16비트 코드를 사용하기 전에 반드시 해야 할 일들이 있다 :
16비트 코드에서 사용될 절대 주소들을 /dev/mem 으로부터 여러분의 프로세스의 주소 공간으로 메모리 맵 하라. (이를테면, ROM 이나, 비디오 버퍼, DMA 타겟들, 그리고, memory-mapped I/O 와 같은 것들)
LDT 와(나) vm86 모드의 모니터를 셋업하라 (setup the LDT and/or vm86 mode monitor)
적절한 I/O 접근 권한을 커널로부터 획득하라(위의 섹션을 참조하라)
다시한번 DOSEMU 프로젝트의 소스들에 나오는 내용들을 주의깊이 읽도록 하여라. 특히, 리눅스/i386 에서 ELKS 와 간단한 .COM 프로그램들을 돌릴 수 있는 mini-emulator 들의 소스를 자세히 보도록 하라.