어셈블리 명령어 REPE SCASB 분석
어셈블리 명령어인 REPE SCASB에 대해 분석한다
0. REPE SCASB에 대한 글을 작성 하면서..
Assembly 예제 코드들을 보면 문자열을 다루는 부분에서 Repeat 접두사와 스트링 프리미티브 명령어(String Primitive Instruction)의 조합이 자주 등장한다.
필자의 경우 이 REPE SCASB 의 연산 결과를 단순하게 조건 분기문(ex: cmp)이나 루프문의 그것과 같이 단정짓고서 소스 코드를 분석하여, 특히나 인덱싱 작업 등을 할때 그 실제 결과 값이 당초 예상했던 결과 값과 다르게 산출되어 적잖이 애를 먹었던 경험이 더러 있다.
이는 필자의 Repeat 접두사 + String Primitive Instruction 에 대한 몰이해 때문이었다.
이는 위와 같은 이유에서 비롯되는 여러 실수들을 미연에 방지하고 좀더 정확한 연산 결과를 추론하기 위해서는 Repeat 접두사 + String Primitive Instruction에 대한 이해가 반드시 필요하다고 여겨질 수 밖에 없는 대목인 것이다.
그런 이유에서 REPE SCASB 구문과 동일 하게 작동하도록 구현된 코드를 통해 Repeat 접두사 + String Primitive Instruction 내부 매커니즘을 살펴 보는 것이 이 글의 주 목적이다.
물론 아래 나오게 될 내부 구현 코드라고 해도 결국은 인덱싱 값과 여러 레지스터들의 결과 값들을 추적하여 이를 통해 필자가 직접 구현한 것이기 때문에 당연하게도 실제 내부 코드와 정확하게 일치하지 않을 것이다.
설령 그렇다 하더라도 내부 작동 원리라는 큰 줄기를 잡고 이해하는데 있어 최소한의 도움은 될 것이라 생각한다.
더불어 스트링 프리미티브 명령어의 구성 요소들의 개략적인 내용들 또한 다루어 보겠다.
1. 스트링 프리미티브 명령어
| 명령어 | 동작 설명 |
|---|---|
| MOVSB, MOVSW, MOVSD | 데이터를 ESI가 가리키는 메모리에서 EDI가 가리키는 메모리로 복사. 각각 Byte(8비트), Word(16비트), Double Word(32비트)를 다룸. |
| CMPSB, CMPSW, CMPSD | ESI와 EDI가 가리키는 두 메모리의 내용을 비교. |
| SCASB, SCASW, SCASD | 누산기(AL, AX, EAX)와 EDI가 가리키는 메모리 내용을 비교. |
| STOSB, STOSW, STOSD | 누산기의 내용을 EDI가 가리키는 메모리에 저장. |
| LODSB, LODSW, LODSD | ESI가 가리키는 메모리를 누산기로 적재. |
2. Repeat 접두사
ECX를 카운터로 사용하는 루프 작업을 위한 접두사로써 접미사로 오는 스트링 프리미티브 명령어를 반복적으로 수행 시킨다. 리스트 파일에는 기계어 코드 F3으로 번역 되어 진다(VS2010 기준).
3. 방향 플래그
스트링 프리미티브 명령어는 방향 플래그의 상태에 따라서 ESI와 EDI를 증가시키거나 감소시킨다. CLD : 방향 플래그 값 0으로 해제 : 증가 : 낮은 주소 -> 높은 주소 STD : 방향 플래그 값 1로 설정 : 감소 : 높은 주소 -> 낮은 주소
스트링 프리미티브 명령어의 경우 단독으로는 하나의 메모리 값 또는 한 쌍의 값만을 처리 할 수 있기 때문에 LOOP 명령어와 같이 ECX를 카운터로 사용하는 Repeat 접두사를 추가 하여 쓰는 것임을 잊지 말자.
4. 예제 코드로 살펴 보는 스트링 프리미티브 명령어와 그 결과 값
다음 예제 코드는 Irvine 교수의 어셈블리 언어 제 5판 9장에 실려 있는 Str_trim 프로시저 코드다. 문자열의 마지막 위치에서 처음 위치라는 역방향 인덱싱을 통해 선택된 문자(Trailing Character)를 모두 제거하는, 엄밀히 말하면 선택된 문자의 위치에 널 문자를 삽입함으로써 이를 제거하는 효과를 거두는 기능을 수행한다.
<StrTrim 예제 : 어셈블리 언어 제 5판>
다음의 코드는 위의 코드에서 repe scasb 수행 직후의 결과를 살펴 보기 위해 call DumpRegs 구문을 삽입한 코드다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
StrTrim PROC USES eax ecx edi,
pString:PTR BYTE,
char: BYTE
;Remove all occurrences of a given character from
;the end of a string.
;returns: nothing
;--------------------------------------------
mov edi, pString
INVOKE Str_length, edi ; returns length in EAX
cmp eax, 0 ; Zero-length ?
je L2 ; yes : exit
mov ecx, eax ; no : counter = string length
dec eax
add edi, eax ; EDI points to last char
mov al, char ; char to trim
std ; direction = reserve
repe scasb ; skip past trim character
call DumpRegs
jne L1 ; removed first character?
dec edi ; adjust EDI: ZF = 1 && ECX = 0
L1: mov BYTE PTR [edi+2], 0 ; insert null byte
L2: ret
위의 프로시저를 호출 및 사용하는 방법은 아래와 같다.
“Hello##” 과 같은 문자열이 있다고 가정했을때, 뒤의 “##”을 삭제 하고 온전히 “Hello” 문자열만 얻고 싶다면
INVOKE StrTrim, ADDR str, ‘#’
위와 같이 호출 하면 되는 것이다.
첫번째 인자는 수정할 문자열의 주소값이고, 두번째 인자는 제거 할 문자 값이다.
이제 사용 방법을 살펴 봤으니 repe scasb 의 연산 결과를 이 바로 다음 줄의 DumpRegs 프로시저 호출문을 통해 살펴 보도록 하자.
연산 결과는 아래와 같다.
[어셈블리 언어 5판 표 9-4]
| 문자열 정의 | SCASB 중단 시 EDI | ZERO 플래그 | ECX | 널 저장 위치 | |————————|——————-|————-|—–|————–| | str BYTE “Hello##”,0 | str + 3 | 0 | 4 | [edi + 2] | | str BYTE “#”,0 | str - 1 | 1 | 0 | [edi + 1] | | str BYTE “Hello”,0 | str + 3 | 0 | 4 | [edi + 2] | | str BYTE “H”,0 | str - 1 | 0 | 0 | [edi + 2] | | str BYTE “##”,0 | str + 0 | 0 | 1 | [edi + 2] |
위의 테이블에서 첫 번째 행을 살펴 보자.
이는 str BYTE “Hello##”,0 에서 널문자인 0을 제외하고 # 문자를 삭제하여 “Hello”,0 라는 값을 얻고자 하였을때의 결과를 정리한 행이다.
REPE SCASB 수행이 끝나면 EDI는 str + 3의 위치, 즉 문자열의 3번째 요소인 문자 ‘l’을 가리키고 있으며 문자열에 아무런 데이터도 남지 않는 상태인지를 알려주는 Zero 플래그가 0으로 세팅되어 있어 현재 Trim 작업이 끝난 str에 데이터가 남아 있음을 알 수 있다.
또한 문자열의 끝에서 부터 시작 번지인 베이스 번지로 감산 카운팅되는 ECX 값이 ㅇㄹㅇㄹ 이고 , NULL 문자는 ‘l’로 부터 2번 더 이동한 위치인, edi로 부터 +2 바이트의 위치에 삽입되었다는 사실을 확인할 수 있다.
만약 str_trim 프로시저가 어떤 식으로 작동 되는지 모르고 있는 상태에서 위의 행을 본다면 “어라? edi가 ‘o’를 가리키고 edi+1 위치에 널문자가 삽입되어야 하는거 아닌가?” 라며 반신반의 할 것이다.
이러한 혼란에 대한 부분은 포스팅 시작 부분에서 이미 언급 했었다. :)
5. REPE SCASB 전격 해부!! 위 코드와 같은 기능을 수행하는 프로시저를 구현해 보자
repe scasb의 내부 모습을 전개하면 아래와 같을 것이다.
1
2
3
4
5
6
7
8
9
10
Lable: mov bl, [edi]
dec edi
dec ecx
cmp al, bl
jne Quit
cmp ecx, 0
je Quit
jmp Lable
Quit: cmp al, [edi+1]
01 ~ 03 :
ecx 카운터가 0에 이르렀는지와 기준 문자와 문자열의 문자 값을 비교하기에 앞서 esi와 edi가 가리키고 있는 곳의 값을 다른 레지스터에 미리 옮겨 놓은 뒤 esi,edi가 가리키는 주소값과 ecx의 값을 각각 형식 크기에 따라 감산하는 인덱싱 작업을 수행 하고 있는 것을 볼 수 있다.
이것이 Repeat 접두사 + String Primitive Instruction 내부 매커니즘의 요체라고 할 수 있을 것이다.
위의 <결과표>에서 1행을 살펴보는 예에서 발생 했던결과표>
“어째서 edi가 ‘o’가 아니라 ‘l’를 가리키고 있는 것인가?” 하는 혼란을 해결해 주는 실마리가 되는 것이다.
문자열 str + 4의 ‘o’ 와 기준 문자 ‘#’를 비교하기 전에 이미 edi의 1바이트 감산 작업이 수행되었기 때문에 repe scasb 수행이 끝났을때 edi가 str + 4의 ‘o’가 아닌 str + 3의 ‘l’을 가리키고 있는 것이다.
ecx의 경우도 마찬가지다.
<결과표> 3행의 ECX 결과를 보면 알 수 있듯이 분명 "Hello",0 으로써 Trim 대상인 기준 문자 '#'가 문자열에 포함되어 있지 않음에도 ecx가 1 감소 되는 이유 또한 ecx 감산 작업이 cmp ecx, 0 수행 전에 이루어졌기 때문이다. 비교 구문 전에 edi와 ecx가 먼저 감산 수행 된다는 것을 잊지 말자. ### 05 ~ 09 : --- 05~09 라인의 내용을 한마디로 정리하면 ```c if((al == bl)&&(ecx>0)) goto Lable ``` 와 같다. 기준 문자와 문자열의 문자간의 비교, 카운터가 0이 되는지의 유무를 동시에 판별하기 위해서는 AND 연산이 가장 적합한 것이다. ### 11 : --- 만약 ecx가 0이 되었을때 zero 플래그가 1로 세팅된다면 이는 위의 <결과표> 마지막 4행의 Zero 플래그 결과 값에 위배된다. Zero 플래그 값을 차치하고서라도 만약 StrTrim 예제의 jne L1 명령문이 ecx의 cmp 연산에 영향을 받는다면 <결과표> 2행의 "#",0 에서 # 자리에 널 문자를 삽입하기 위한 사전 작업을 하는 dec edi 구문이 실행되지 않아 옳지 않은 결과 값이 나오게 된다. 그렇기 때문에 11번 줄의 > `Quit: cmp al, [edi+1]` 명령문으로써 그 대미를 장식하게 되는 것이다. 또한 이것은 문자열에 "#" 이나 "##" 등과 같이 오로지 삭제하기를 원하는 기준 문자 '#'만으로 구성된 문자열인지 판별하는 아주 중요한 기능을 수행한다. 이를 통해 jne L1 에서 L1으로 분기하지 않도록 하여 dec edi 를 수행할 수 있게 해주는 것이다. ## 6. 마치며.. 쓰고 나니 상당히 조악한 글이 되고 말았다. 당초 작성하기전 구상하고 생각했던 것과는 다르게 내용의 구성이 부실하고 전개 또한 매끄럽지 못해 만족스럽지 못하다. 쓰는 동안 나도 모르게 귀차니즘이 찾아들었던 모양이다. 필자인 본인 스스로가 공부하고 알아낸 내용들을 잊지 않기 위해 복습 차원에서 작성하는 것이 일차적 이유인데 훗날 잊고 살다가 다시금 읽었을때 어떨지.... 여하튼, 내게도 블로그를 찾아오시는 다른 분들께도 많은 도움이 되었으면 좋겠다. 결과표>결과표>결과표>