5.1. Linux

5.1.1. GCC 로의 링킹 : Linking to GCC

만약 여러분이 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 매크로를 어떻게 정의하고 있는지도 참조하라.

5.1.2. ELF 와 a.out 문제

몇몇 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);
그렇게 하면, foo() C 함수는 실제로 어셈블리에서는 bar 이라고 불리우게 될 것이다.

binutils 패키지의 objcopy 유틸리티가 여러분의 a.out 오브젝트를 ELF 형식의 오브젝트 파일로 변환해 줄 수 있음을 주목하라. 그리고, 그 반대의 일도 가능할 것이다. 보다 일반적으로 이야기하자면, 그 유틸리티는 더 많은 형태의 파일 형식 변환도 해 줄 것이다.

5.1.3. Direct Linux syscalls

여러분은 종종 C library (libc) 를 사용하는 것이 유일한 방법이며, 직접적인 시스템 콜은 별로 좋지 않다는 말을 들을 것이다. 이것은 사실이다. 그러나 일반적으로 여러분은 libc 가 신성시되고 있지 않다는 것과, 대부분의 경우 libc 는 몇가지 체크만 하고, 커널을 호출한 후, errno 변수를세팅할 뿐이라는 것을 알아야 한다. 여러분은 여러분의 프로그램에서도 이와 같은 것을 쉽게 할 수 있다 (만약 여러분이 그것을 필요로 한다면). 그리고 여러분이 공유 라이브러리를 사용하지 않는다는 이유만으로도 여러분의 프로그램은 몇십배 더 크기가 줄어들 것이며, 수행성능 또한 더 좋아질 것이다. libc 을 어셈블리 프로그래밍에서 사용하거나 하지 않거나 하는 문제는 실용적인 문제라기 보다는 기호나 신념에 관한 문제이다. Linux 는 POSIX 호환을 목표로 하고 있다는 것을 기억하라. 그리고, libc 는 그러한 목표를 만족한다. 이것은 libc system call 의 거의 모든 문법이 커널의 시스템 콜의 실제 문법과 정확히 일치한다는 뜻이다. 그러나 GNU libc(glibc) 는 버젼업이 되어 갈수록 더 느려지고 있다. 또한 메모리도 더 많이 잡아먹어 가고 있다. 그래서 직접적인 시스템 콜이 꽤나 실용적인 것이 되어가고 있다. 그렇지만... libc 를 사용하지 않음으로써 생기는 주된 부작용(?, drawback) 은 아마도 여러분이 몇몇의 libc 의 특정 함수들(시스템 콜 wrapper 가 아닌)을 직접 구현할 필요가 생길지도 모른다는 것이다. (이를테면, printf() 과 같은...) 그리고, 여러분이 libc 를 사용하지 않기로 결정했다면, 당연히 여러분은 그렇게 할 각오(?)가 되어 있는 것이다. 그렇지 않은가?

여기에 직접적인 시스템 콜을 하는 것의 득과 실이 나열되어 있다 :

이득:

손해:

만약 여러분이 방금 언급한 득실들을 심사숙고 했으며, 그래도 여전히 direct 시스템 콜을 사용하기를 원한다면, 아래에 몇가지 조언을 첨부해 주겠다 :

기본적으로, 시스템 콜은 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/).

5.1.4. 리눅스에서의 하드웨어 입출력

여러분이 리눅스에서 직접 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 인라인 어셈블리와 매크로를 사용함으로써 전체를 어셈블리로 코딩하는 것보다 효율적으로 일을 수행할 수 있다.

5.1.5. Accessing 16-bit drivers from Linux/i386

그러한 일들은 이론적으로 가능하다 (증명 : 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비트 코드를 사용하기 전에 반드시 해야 할 일들이 있다 :

다시한번 DOSEMU 프로젝트의 소스들에 나오는 내용들을 주의깊이 읽도록 하여라. 특히, 리눅스/i386 에서 ELKS 와 간단한 .COM 프로그램들을 돌릴 수 있는 mini-emulator 들의 소스를 자세히 보도록 하라.