델파이에서 어셈블러 사용하기 (1)

사용자 삽입 이미지 이 아티클은 귀도 자이벨스(Guido Gybels)의 Using Assembler in Delphi를 번역한 것으로, 총 4개의 장으로 되어 있는 시리즈 아티클들 중 첫번째입니다. 번역 및 전재를 하도록 허락해주신 귀도씨에게 감사드립니다.

원문 : http://www.guidogybels.eu/asmch1.html

 

1장. 어셈블리 코드 사용의 일반 사항들

델파이 프로젝트 내에서 어셈블러를 제대로 이용하기 위해서는, 어셈블러로 작성된 루틴을 호출하는 방법과 파라미터에 액세스하는 방법, 그리고 결과를 리턴하는 방법을 이해해야 합니다. 이 장에서는, 어셈블러 코드를 써야 하는 위치와 기본적인 구조에 대해 알아보겠습니다. 컴파일러가 엔트리 및 엑시트 코드를 생성하는 동작에 대해서도 설명할 것입니다. 노트: 이 문서에서는, 예제들에 사용된 콜링 컨벤션을 항상 명시적으로 지정할 것입니다. register일 경우에는 굳이 지정하지 않아도 되지만(콜링 컨벤션이 명시적으로 지정되지 않을 경우 기본 컨벤션이기 때문), 코드를 보는 사람에게 파라미터가 레지스터에 있다는 것을 기억하게 해주기 때문에 가독성에 도움이 됩니다(추가적인 주석이라고 생각해도 되겠습니다). 이 팁은 Christen Fihl이 조언해준 것입니다.

1.1. 어셈블러 코드의 위치

델파이에서 어셈블러 코드를 포함시키려면, 어셈블러 블럭을 표시해주는 asm을 사용해야 합니다. 블럭의 끝은 end로 표시합니다.


파스칼 함수나 프로시저 내에서 asm 블럭을 포함시키는 것도 가능하기는 하지만 그런 방식을 권장하지는 않습니다. 어셈블러는 별도의 함수나 프로시저 블럭으로 분리해주는 것이 좋습니다. 가장 먼저, 일반적인 파스칼 함수에 어셈블러 코드를 넣게 되면 컴파일러의 최적화와 변수 관리 동작에 영향을 주게 됩니다. 따라서 생성된 코드는 최적화와는 거리가 멀어지게 됩니다. 변수들이 레지스터 바깥으로 푸시될 가능성이 높아서 스택에 저장된 후 다시 불러와야 할 수 있습니다. 또한, 파스칼 블럭에 어셈블러 코드를 추가하게 되면 컴파일러가 생성한 코드가 여러분의 어셈블러 코드와 연결되게 됩니다. 이것은 최적화 로직을 방해하고 그 결과는 상당히 비효율적이 됩니다. 따라서, 어셈블러 코드는 자체 개별적인 함수/프로시저 블럭으로 코딩하는 것을 원칙으로 합니다. 설계상의 관점도 이슈가 됩니다. 모든 어셈블러 코드가 명확하게 격리되고 잘 주석처리된 블럭으로 코딩되면 가독성과 유지보수성도 크게 높아집니다.

 

1.2. 레이블

레이블은 코드에서 어떤 위치를 표시하는 태그입니다. 레이블을 사용하는 가장 일반적인 이유는 분기(branch)의 참조 위치를 만들기 위한 것입니다. 어셈블러 코드에서 사용할 수 있는 두 가지의 레이블이 있는데, 파스칼 스타일 레이블과 지역 어셈블리 레이블입니다. 전자는 먼저 레이블 섹션에 선언해야 합니다. 일단 선언되고 나면 그 레이블을 코드에서 사용할 수 있습니다. 레이블 뒤에는 반드시 콜론(:)을 붙여야 합니다.

위의 예는 MyLabel이라는 이름의 레이블을 선언하는 방법과 코드 내에서 위치를 표시하는 방법(MyLabel:), 그리고 점프 명령으로 레이블의 위치로 이동하는 방법(jnz MyLabel)을 보여줍니다. 동일한 코드를 약간 더 간단한 방법으로 할 수도 있는데, 어셈블러 코드 내에서 지역 레이블을 사용하는 것입니다. 지역 레이블은 선언을 할 필요가 없이 단순히 레이블을 독립적인 문장으로서 추가하기만 하면 됩니다. 지역 레이블은 @ 기호로 시작해야 하며, 역시 콜론(:)을 뒤에 붙여줘야 합니다. @ 기호는 파스칼 식별자의 일부가 될 수 없으므로 지역 레이블은 asm…end 블럭 내에서만 사용할 수 있습니다. 어떨 때는 제 웹사이트에서 내려받은 코드 등에서 두 개의 @ 기호가 붙은 레이블을 보게 될 수도 있습니다. 이것은 특별한 표시를 하려는 제 방식이며 그렇게 해야 하는 것은 아닙니다. (어떤 어셈블러는 @@을 특별한 목적이 레이블로 인식하기도 합니다. 예를 들면 @@: 이런 레이블은 익명의 레이블을 의미하기도 합니다) 아래 예는 위의 코드와 동일하지만 지역 레이블을 사용한 것입니다.


두 가지의 레이블은 본질적으로 동등합니다. 코드 크기나 속도에 있어서도 차이가 없습니다. 레이블은 컴파일러에게 단순히 오프셋과 점프를 계산하기 위한 참조 위치일 뿐이기 때문입니다. 어셈블러 블럭에서 파스칼 스타일 레이블과 지역 레이블의 차이는 과거의 유물이며 사라져가고 있습니다. 따라서, 현재 함수나 프로시저 블럭 바깥에 있는 레이블로 점프할 수는 없다는 점에서 파스칼 스타일 레이블도 ‘지역적’입니다. 함수나 프로시저 바깥으로의 점프는 심각한 문제를 일으킬 수 있으므로, 그렇게 할 수 없도록 되어 있는 것이 오히려 다행이죠.

 

1.3. 루프

어셈블러 코드를 사용하는 목적은 가능한 한 최고의 속도를 내기 위해서일 경우가 많습니다. 또한 루프 내에서 대량의 데이터를 처리해야 하는 경우도 상당히 자주 있는 일입니다. 루프를 사용해야 하는 상황이라면 루프 자체도 어셈블러로 구현해야 합니다. 호출 오버헤드 때문에 많은 실행 시간을 낭비하는 경우가 아니라면 루프를 어셈블러로 구현하는 일은 어려운 일이 아닙니다. 따라서, 다음과 같은 코드 대신에,


어셈블러 루틴 내에서 루프를 구현해야 합니다.


위의 예에서, 루프 카운터가 아래 방향으로 카운트하는 것을 보십시오. 이렇게 하면 루프의 끝에 도달했는지 알아보기 위해 감소 후에 0인지만 검사하기만 하면 되기 때문입니다. 반대로, ecx=0로 시작해서 위쪽으로 카운트한다면 루프를 계속할 것인지를 검사하기 위해 추가로 비교 연산을 해야 합니다.


다른 방법으로, 0에서 횟수를 빼고 0에 도달할 때까지 증가시키는 방법도 있습니다. 이 방식은 루프 인덱스 레지스터를 메모리의 테이블이나 배열에 대한 인덱스로 사용하려고 할 경우 특히 유용합니다. 전진 방향으로 데이터를 액세스하는 것이 캐시 성능에 더 낫기 때문입니다.

이런 방식을 사용할 경우, 기본 레지스터 혹은 주소는 반드시 배열 혹은 테이블의 시작이 아닌 끝 위치를 가리켜서 각 요소들을 거꾸로 순환하도록 해야 합니다.

1.4. 엔트리 및 엑시트 코드

또 하나 기억해둬야 할 중요한 것은, 여러분의 어셈블러 블럭에 대해 컴파일러가 자동으로 엔트리 및 엑시트 코드를 생성한다는 것입니다. 이것은 스택 프레임이 필요한 경우에만 해당하는데, 예를 들면 파라미터가 스택을 통해 해당 루틴으로 전달될 경우나 로컬 데이터가 스택에 저장될 때입니다. 이런 경우가 대단히 흔하기 때문에 결과적으로 엔트리 및 엑시트 코드가 생성됩니다. 컴파일러는 다음과 같은 엔트리 코드를 만들어냅니다.

이 코드는 ebp를 보존하고 스택 포인터를 ebp 레지스터에 복사합니다. 이후로는 스택 프레임에 대한 정보를 액세스하기 위한 기본 레지스터로 사용할 수 있게 됩니다. sub  esp 라인은 필요한 지역 변수들을 위한 스택 공간을 예약합니다. 엑시트 코드 패턴은 다음과 같습니다.


엑시트 코드는 ebp(스택 프레임의 시작 위치를 가리키고 있음)를 스택 포인터에 다시 복사하여 지역 파라미터들에 할당된 공간을 정리합니다. 이렇게 하면 지역 변수들에 사용된 공간이 할당 해제됩니다. 다음으로, ebp가 루틴의 시작 때 가지고 있었던 값으로 복원됩니다. 마지막으로, 호출한 측으로 제어를 다시 넘겨주고, 해당 루틴으로 전달했던 파라미터들에 할당된 공간을 위한 스택을 다시 조절합니다. ret 명령에서 이런 파라미터 정리는 cdecl을 제외한 모든 콜링 컨벤션에서 필요합니다. cdecl을 제외한 모든 경우에 호출된 함수가 파라미터를 위해 할당된 스택 공간을 정리할 책임을 가지게 되며, 따라서 ret 명령이 필요한 조정 작업을 포함하게 됩니다. 하지만 cdecl의 경우에는 호출한 함수가 정리를 수행하게 됩니다.

여러분의 함수 혹은 프로시저가 스택을 통해 전달하는 파라미터도 없고 지역 변수도 없다면, 엔트리 및 엑시트 코드는 전혀 생성되지 않으며, 다만 ret 명령은 항상 생성됩니다.

1.5. 레지스터 보존

여러분의 함수 혹은 프로시저 내에서 레지스터들을 사용할 때는, eax, ecx, edx 레지스터들만자유롭게 수정할 수 있다는 점을 주의하십시오. 모든 다른 레지스터들은 보존해야 합니다. 즉, 만약 루틴에서 다른 레지스터들을 사용했다면 반드시 값을 저장해뒀다가 리턴하기 전에 되돌려놔야 합니다. 세그먼트 셀렉터의 내용은 변경하면 안됩니다. ds, es, ss는 모두 동일한 세그먼트를 가리키고, cs는 독자적인 값을 가집니다. fs는 쓰레드 정보 블럭(TIB)를 가리키며 gs는 예약되어 있습니다. esp 레지스터는 물론 스택의 가장 위를 가리키고, ebp는 컴파일러에 의해 생성된 기본 엔트리 코드의 결과로 현재의 스택 프레임을 가리키게 됩니다. 모든 pop 및 push 동작은 esp 레지스터의 내용을 변경하므로, 일반적으로 esp를 직접 통해 스택 프레임을 액세스하는 것은 좋은 생각이 아닙니다. 대신 그런 목적으로는 ebp를 예약해둬야 합니다. 표 1에서 레지스터의 사용법을 정리해놓았으니 참고하시기 바랍니다. 레지스터의 문맥과 별개로, 디렉션 플래그(df)는 엔트리 직후에 클리어한 있는 상태라고 간주해도 되며, 만약 df의 값을 변경했다면(권장하지 않지만), 리턴하기 전에 클리어한 상태로 돌려놓아야 합니다(cld 명령을 이용). 마지막으로, FPU 컨트롤 워드를 변경하는 데 있어 조심해야 합니다. FPU 컨트롤 워드를 이용하면 부동소수점 연산에서 정도(precision)와 반올림 모드를 바꿀 수 있고 또 특정한 예외들을 차단할 수 있게 해주지만, 여러분의 전체 애플리케이션 전체의 성능에 엄청난 영향을 미치게 됩니다. FPU 컨트롤 워드의 변경이 필요하다고 결정했다면 최대한 빨리 그 값을 복원시켜야 합니다. Comp 혹은 Currency 타입을 사용하는 경우 부동소수점 정도(precision)가 줄어들지 않는지 주의해야 합니다.

표 1: CPU 레지스터의 사용

이 표는 델파이 32비트 애플리케이션에서의 레지스터 사용에 대해 정리한 것입니다. 첫번째 컬럼은 각각의 CPU 레지스터들입니다. 두번째 컬럼은 엔트리 시점에서 해당 레지스터가 가지고 있는 값이며, 세번째 컬럼은 엑시트 시점에 가지고 있는 값입니다. 네번째 컬럼은 해당 레지스터를 여러분의 코드에서 사용해도 되는지의 여부이며, 마지막 컬럼은 해당 레지스터의 내용을 보존할 필요가 있는지를 나타냅니다(엔트리 시점에서 저장하고 엑시트 시점 이전에 복원해야 합니다).

엔트리 엑시트 사용 가능? 보존?
eax self(1), 첫번째 파라미터(2) 혹은 불확실(3) 함수 결과(4) tickmark 아니오
ebx 알 수 없음 n/a tickmark
ecx 두번째 파라미터(1), 세번째 파라미터(2), 혹은 불확실(3) n/a tickmark 아니오
edx 첫번째 파라미터(1), 두번째 파라미터(2) 혹은 불확실(3) 결과 타입이 Int64인 경우 상위dword이며, 그 외의 경우 n/a tickmark 아니오
esi 불확실 n/a tickmark
edi 불확실 n/a tickmark
ebp 스택 프레임 포인터 스택 프레임 포인터 tickmark(5)
esp 스택 포인터 스택 포인터 tickmark(5)
cs 코드 셀렉터 n/a crossmark
ds 데이터 셀렉터 n/a crossmark
es 데이터 셀렉터 n/a crossmark
fs 쓰레드 정보 블럭(TIB) 셀렉터 n/a crossmark
gs 예약됨 n/a crossmark
ss 스택 셀렉터 스택 셀렉터 crossmark

1 comment for “델파이에서 어셈블러 사용하기 (1)

답글 남기기

이메일 주소는 공개되지 않습니다.