편견이 깨지는 어셈블리 프로그래밍 - 최적화 강좌 1 - 3  
 
 





브랜치 유닛
파 이프 라인의 등장으로 우리는 CPU의 클럭 속도를 더 높일 수 있게 되었다. 하지만 파이프 라인에도 약점은 존재한다. 바로 파이프 라인을 채우는 데 걸리는 시간이다(파이프 라인 참고) 파이프 라인은 파이프가 채워져 있을 때만 제 속도를 낼 수 있는 시스템인데 이 파이프가 비워지는 경우가 있다. 그 중 한 가지 경우가 하나의 CPU가 리셋됐을 때로(이 경우는 어쩔 수 없다) 다른 하나는 프로그램 명령 흐름 제어가 바뀌었을 때이다.
즉 JMP 명령 등 브랜치(branch) 명령이 발생했을 때다. 그래서 JMP 명령(C 언어의 경우 for, if, while, select 등의 명령어)을 포함하는 브랜치가 잦은 프로그램일수록 파이프 공정을 비우고 다시 채우는 현상이 빈번하게 발생하므로 파이프를 다시 채워야 하는 시간으로 인하여 수행 속도의 감소 현상이 일어나게 된다. 하지만 이러한 브랜치 명령에 의한 수행속도 감소를 없애기 위해 브랜치 명령을 쓰지 않는다는 것은 불가능한 일이다.
그러므로 최대한으로 브랜치에 의한 수행 속도 감소를 없애기 위해 등장한 것이 브랜치 예측 유닛이다. 브랜치 예측 유닛은 브랜치 타겟 버퍼(BTB)에 실행되었던 예측되는 브랜치 대상 영역의 코드들을 가지고 있다가 브랜치가 실제 발생되었을 때 브랜치 타겟 버퍼들 중에서 해당 브랜치 주소와 일치하는 것이 있으면 명령 큐를 바로 브랜치 타겟 버퍼의 내용으로 교환함으로써 브랜치로 인한 파이프 채움 대기 시간을 최대한으로 줄이는 방식과 명령으로부터 예측되는 브랜치 대상 주소로부터 명령들을 가져올 준비를 하고 있는 방식에 의하여 브랜치에 대한 수행속도 피해를 줄이고 있다.
그럼 펜티엄 Ⅲ의 브랜치 예측 방식에 대해 알아보자. 펜티엄 Ⅲ에는 512개의 브랜치 타겟 버퍼가 존재한다. 펜티엄 Ⅲ의 브랜치 예측 방식에는 동적 브랜치에 대한 예측과 정적 브랜치에 대한 예측 이렇게 2가지 방식이 있는데 이 두 가지는 어떻게 다른 지 알아보자. 동적 브랜치란 간접 주소에 의한 브랜치(예: jmp[bx])에 대해 예측을 해주는 것을 말하고 브랜치 예측 방식은 패턴 매칭 방식과 최후 4개의 브랜치했던 주소를 기억하는 방식으로 구성되어 있다. 특히 루프의 경우 4개의 브랜치 예측 주소에 당첨될 확률이 높다. 또한 펜티엄 Ⅱ, Ⅲ부터 채택된 ret 명령에 의한 예측 시스템도 있다. 원래 브랜치 명령은 jmp/call 명령들을 디코딩(분석)하면서 나온 주소를 보관하는 방식인데 ret 명령의 경우 돌아갈 주소가 스택에 있기 때문에 주소를 얻기가 힘들었다.
하지만 인텔에서는 스택의 주소까지 긁어오는 과감한 브랜치 예측을 포함시키게 된 것이다. 정적인 브랜치(예: jmp Lvl1) 분기 예측방식은 해당 주소를 가져와서 예측을 할 수 있는 방법을 말한다. 브랜치를 100% 정확히 예측할 수 없다. 그럼 브랜치에 한 피해는 어떤 경우에 발생하는지 알아보자 .

◆ 어쩔 수 없는 브랜치의 피해
브 랜치의 대상이 브랜치 타겟 버퍼 내에 없어 브랜치 명령으로부터 가져온 브랜치 예측 주소를 사용할 때를 말한다. 브랜치 타겟 버퍼가 대부분 실행되었던 코드들을 캐시하기 때문에 프로그램의 앞부분이 아닌 실행된 적이 없었던 뒷부분으로 브랜치가 발생할 경우 브랜치 예측 주소로 최대한 빠르게 명령을 가져오는 대상을 바꿔 그 피해를 최소화하는 수밖에 없다. 이럴 경우 보통 대략 5클럭 정도의 손해를 보게 된다.

◆ 운 좋은 브랜치의 경우
브랜치 대상이 브랜치 타겟 버퍼에 존재할 경우이다. 이는 이미 캐시된 코드 데이터가 파이프 라인으로 주입되기 때문에 파이프의 흐름에 피해를 최소화시킬 수 있게 된다. 이럴 경우 보통 한 클럭 정도만이 손해를 보게 된다.

◆ 최악의 경우
이 는 주로 동적 브랜치에 주로 발생하는데 브랜치 대상이 브랜치 타겟 버퍼에 존재하지 않고 명령으로부터 예측했던 브랜치 예측주소와도 맞지 않을 경우이다. 이럴 경우 어쩔 수 없이 파이프 라인을 초기화하고 명령을 가져오는 대상을 브랜치 주소로 바꿔야 하므로 많은 시간을 손해보게 된다. 보통은 10에서 15클럭을 손해보며 최악의 경우 26클럭까지 손해보게 된다.

브랜치 명령으로 인해 손해보는 경우가 커 보이지 않을지도 모른다. 386 시절에는 명령어 하나당 3에서 11클럭 사이였기 때문이다. 하지만 펜티엄이후 명령의 디코딩/실행 시퀄()의 비약적 발전으로 인해 U/V 파이프에 최적화된 명령은 한 클럭에 2개의 명령을 동시에 수행할 수 있어 5클럭이라 해도 무시할 수 없는 크기가 된다. 그렇다면 최적화를 위한 브랜치와 관련해 몇 가지 내용을 정리할 수 있다.

① 최대한 브랜치 발생을 자제하고(while , if , for 등 자제)
② 브랜치를 쓸 경우 간접 주소 지정에 의한 브랜치를 최대한 피할 것(동적인 브랜치)
③ 브랜치와 브랜치 사이는 되도록이면 띄우고, 자주 순환되는 루프들의 경우 short의 점프 거리를 벗어나지 않을 것(+127,-128 바이트 이내)


출처: http://www.imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&wr_id=72



편견이 깨지는 어셈블리 프로그래밍 - 최적화 강좌 1 - 1  
 
 
아무리 하드웨어가 발전한다 해도 높아진 사용자의 눈을 따라 올 수는 없었다. 개발자들은 사용자를 더 만족시키기 위해 하드웨어의 모든 자원을 짜내지 않으면 안 되었고, 그로 인해 부각되는 분야 중 하나가 바로 최적화이다. 멀고도 어렵게 느껴진 최적화, 관련 툴의 발전으로 더 이상 일부 프로그래머들의 전유물이 아니다. 어떤 것을 공략하기 전에 먼저 대상을 잘 파악해야 한다. 따라서 이번 호부터는 달라진 어셈블리와 PC의 심장인 CPU를 공략해보자.  
 





이 강좌는 어렵게만 생각해왔던 최적화에 고급스럽게 변모한 어셈블러를 이용해 더 쉽게 접근해 보자는 취지로 시작한 강좌이다. 일반적으로 ‘최적화’라는 단어가 나오면 ‘어렵다!’, ‘자료가 부족하다’, ‘컴파일러에서 지원해주는 것이 아니냐?’ 등의 이야기를 많이 한다. 하지만 조금만 관심을 가지고 시스템을 이해한다면 아주 쉬운 방법으로 최적화된 코드를 작성할 수 있다. 현재 우리가 쓰는 고급 언어들의 컴파일러는 많은 사람들의 노력과 기술의 발전으로 상당히 최적화되어 있다. 하지만 CPU에 특성화되지 않은 범용 언어인 만큼 상황 설정에 맞춘 알고리즘에 의하여 생성된 실행 코드로는 최적화에 한계가 있을 수 있다. 이런 부족한 점을 보완하기 위해 어셈블러 역시 공부할 필요가 있다.
이번 연재는 CPU에 대한 설명인 만큼 CPU 명령과 거의 일대일로 매칭되는 어셈블러를 비중 있게 다룰 것이다. 어셈블리하면 매우 어렵고 쓰기 불편한 언어로 인식되어 왔지만 MASM이 버전 6.0대에 들어서면서 매우 편리하고 고급적인 기능이 추가되었으며 고급 언어와의 연결 또한 용이해지게 되었다. 따라서 여기서는 MASM 7.0 버전을 위주로 설명해 나갈 것이고, 이 컴파일러는 http://www.asmlove.co. kr에서 다운받을 수 있다.

펜티엄 내부 블럭도
코드의 최적화란 해당 CPU의 특성을 가장 잘 이해하고 코드를 작성하는 것이다. 그렇다면 컴퓨터에서 가장 핵심적인 부분인 CPU에 대해 알아 볼 필요가 있다. <그림 1>은 펜티엄의 블럭 다이어그램을 나타낸 것이다. 이 구조를 크게 기능별로 크게 4가지 유닛(Unit)으로 나눌 수 있다.

◆ 메모리 서브시스템
메 모리 서브시스템(memory subsystem)은 노스 브릿지(north bridge)라는 메인 버스 컨트롤러(main bus controller)와 연결해주는 시스템 버스(system bus), 버스 인터페이스 유닛(bus interface unit)과 데이터의 원활한 흐름을 위해 L2 캐시, 인스트럭션 캐시(instruction cache, L1), 데이터 캐시(date cache, L1), 메모리 인터페이스 유닛(memory interface unit) 그리고 메모리 레코더 버퍼(memory reorder buffer)로 구성되어 있다.

◆ 페치/디코더 유닛
인스트럭션 페치 유닛(instruction fetch unit), 브랜치 타겟 버퍼(branch target buffer), 인스트럭션 디코더(instruction decoder), 마이크로코드 시퀀서(microcode sequencer), 레지스터 알리아스 테이블(register alias table)로 구성되어 있다. 이 부분은 실행 유닛이 효율적으로 수행할 수 있게 하기 위해 알맞은 규격의 명령을 가공하는 부분이다. CPU에 대한 최적화는 이 부분을 얼마나 잘 이해하고 활용하느냐 하는 것인데, 이에 관하여 이번 강좌에서 자세하게 다루겠다.

◆ 인스트럭션 풀(instruction pool) 또는 레코더 버퍼(reorder buffer)

◆ 디스패치/실행 유닛(dispatch/execute unit)
이 부분이야 말로 실질적인 명령의 수행이 이뤄지는 곳이다. 이 부분은 실행 가능한 파이프에 명령을 분배하기 위한 예약 스테이션(reservation station)과 두 개의 정수 처리 유닛과 하나의 부동 소수점 유닛 두 개의 주소 번역기 유닛과 두 개의 병렬 데이터 처리 유닛을 포함하고 있다

◆ 리타이어먼트 유닛(retirement unit)
명령어 수행이 최종적으로 끝나는 곳이다.


출처: http://www.imaso.co.kr/?doc=bbs/gnuboard.php&bo_table=article&wr_id=70


전산을 시작하고.....벌써......강산이 바뀌었다.
그 사이에 인터넷이라는 것이 발달을 했고,
참, 귀중한 많은 것들을 ....이름 모를 이들에게서.....
얻었다.......

고개숙여 감사를 드리고 싶다...
이제 나도..뭔가를 뱃어 내야할 때가 온 것 같은데....
가진게 없다보니...뱃을게 없다.? 그래도, 할 만큼을 했다는 소린해야 겠기에....
허접하지만 한 번 뱃어 볼라구 한다.

인라인 어셈이란,
고급언어에서 어셈블러 명령을 직접 사용하는 것을 말한다.
새로 시작하시는 분이나.....처음 접하시는 분들에게는 도움이 되리라
생각한다.
인라인 어셈은 필요한가?...........
-----------------------------------------------

2GHz 대의 펜티엄4 CPU 가 일반화 될려는 이....시기에
어셈이 필요할까?

음..............

글세..........

^_^

필자는 인라인어셈을 안 사용하고 게임을 만들어 본 적이 없어서
잘 모르겠다.......

뭐......배워서 버릴게 있겠는가 !!

CPU의 내부 구조.
-----------------------------------------------

여기서 CPU의 자세한 내부 구조를 설명하지는 않겠다.
왜냐하면 하다 보면......자연히 알게 되니깐....
귀챦은데......미리 알아서 외우느라....골아플 필요가 있겠는가..!!

간단하게만 얘기해 보면,

CPU는 어떠한 처리를 할때,
기본적인 방식이..........

1. RAM에 있는 데이타를 CPU 안에 복사
2. 이를 처리(연산)
3. 결과를 다시 RAM에 저장.

이러한 과정을 거친다....

따라서, CPU 내부에는 RAM의 데이타를 읽어 와서.
임시로 저장할 공간이 있다.
이 공간을 Register(레지스터) 라고 한다.
그 크기는 32bit .......즉 integer 값 하나......정도를 저장할
공간 밖에 안된다.
하지만, 이러한 Register가 하나로 부족해서 몇개가 있다..
이들에게 이름을 붙였는데.

EAX, EBX ...

등과 같은 방법으로 붙였다.
레지스터의 종류나.....그 역활에 대해서는 차차 알아 보자.
보다 자세한 내용을 알고 싶은 분들은.... 어셈관련 참고서적을
참고하기 바란다.

//

이 번에는 소스를 한 번 살펴 보자...

void memset ( void* dest, char fill, int num )
{
if (num <= 0) return;

_asm
{
mov?? al, fill
mov?? ecx, num
mov?? edi, dest
rep?? stosb
}

}
일단 얼핏 보기에도... C 코드와 어셈 코드가 섞여 있다.

잘 알 듯이.

_asm { } 로 둘러 싸여 있는 부분이 어셈 코드이다.

그 안을 살펴 보면,

mov, rep, stosb 는 cpu 명령어 이구..
al, ecx, edi 는 register(레지스터) 이다.

fill, num, dest 는 변수 이다.
변수를 CPU가 어찌 알아차리겠는가.........
하지만 고맙게도.. 컴파일러가 변수들을 각 변수들의 주소로....변환해 준다.
이러한 것은 디버깅시에...살펴 보면 알 수 있다.

잠시 위 코드의 뜻을 살펴 보면 (물론 다 알겠지만)

fill 변수의 값을 al 레지스터로 옮겨 오고,
num 변수의 값을 ecx 레지스터로 옮겨 오고,
dest 변수의 값을 edi 레지스터로 옮겨 오고,
(여기서 dest 변수의 값을 타입에서 알수 있듯이 포이터 이다.)

반복을 하라..... (rep)
어떻게 ..
al 레지스터의 값을
edi 레지스터에 있는 주소로
ecx 레지스터의 값 만큼 반복해서 저장해라. (stosb)

아마도.......이런뜻일 것이다.

위 함수는.......
자주 사용하는 memset 함수를 한 번 만들어 본 것이다.
다음에는 memcpy 를 한 번 만들어 보자.
%% 오늘의 어셈 명령어
-------------------------------------------------
MOV

이 명령어는 데이타를 옮길때 사용한다,
1. 레지스터에 특정 값을 넣거나,
2. 레지스터에서 레지스터로 값을 옮기거나,
3. 메모리(RAM)에 있는 값을 레지스터로 가져 오거나,
4. 레지스터에서 메모리(RAM)로 데이타를 옮길때....

애석하게도 MOV는 메모리(RAM)에서 메모리(RAM)으로
바로 데이타를 옮기지는 못한다.

ex)? MOV? al, 10??? // al 레지스터에 10이라는 값을 넣어라.
MOV? ebx, eax?? // eax 레지스터의 값을 ecx 레지스터에 넣어라.
MOV? ecx, [edi] // edi 레지스터가 가리키는 주소(RAM)에 있는 값을 ecx로 가져 와라.
MOV? [edi], ecx? // ecx 레지스터에 있는 값을 edi 레지스터가 가리키는 주소(RAM)로 옮겨라.

///
자......약속한 대로....오늘은 memcpy를 한 번 만들어 보자.

void memcpy ( void* dest, void* src, int num )
{
if (num <= 0) return;

_asm
{
mov?? edi, dest
mov?? esi, src
mov?? ecx, num
cld
rep?? movsb
}
}

한 번 분석해 보자.
edi, eax, esi, ecx 는 레지스터 일테고,
mov, cld, rep, movsb 는 cpu 명령어 일것이다.
그리고, dest, src, num은 변수 이다.

edi 에 dest 변수 값을 넣고,
esi 에 src 변수 값을 넣고, ( ...쉽~네~ )
ecx 에 num 변수값을 넣을 테고,
음 cld 는 뭘까? .. 일단 무시하고......
rep는 ecx 값만큼 반복하라는 뚯이고,
(그럼, ecx에 num 값이 있으니, num 만큼 반복하겠군..)
저번에는 stosb 였는데 이번에는 movsb 군,

stosb 는 al 의 값을 edi 주소에 저장하는 것이고,
movsb 는 esi주소의 값을 edi 주소로 옮기는 명령이다.
그리구나서, esi의 값과 edi 값을 하나씩 증가 시킨다.

이를 종합해 보면,
rep movsb 는 esi 주소의 값을 edi 주소로 ecx 갯수만큼 옮기는 역활을 한다.
우리가 알고 있는 memcpy 함수와 동일한 역활을 하는 것이다.

자..... 여기서 우리는
esi 레지스터는 소스 주소,
edi 레지스터는 목적지의 주소를,
ecx 레지스터는 카운트의 용도로 쓰임을 볼 수 있다.

프로그래머가 레지스터를 어떻게 사용할 것이냐는...알아서 결정할 문제이지만,
대부분 위와 같은 역활로 쓰인다.
인제 위에서...지나간 cld 에 대해서 알아 보자.
레지스터에는 여러 종류의 레지스터가 있는데....그 중 플래그레지스터 라는 놈이 있다.
이 놈은 자신의 각 비트가 하나의 상태를 나타낸다.
(하나의 비트 이므로 0 아니면 1 의 값만을 갖는다.)
이 놈은 cpu 의 명령이 실행될때마다.....그 결과에 대한 상태의 변화를 기록한다.

mov?? edi, dest

라는 명령을 실행했다고 하면,
플래그 레지스터의 상태를 나타내는 플래그에는
OF (Overflow가 발생했느냐?)
DF (방향- 어느방향으로 진행할까?)
ZF (Zero- 실행 결과가 0 이냐?)
CF (Carry- 실행 결과가 자리 올림이 발생했느냐?)
....
등등이 있다.
자세한 종류는 어셈 관련 서적을 참고하기 바란다...(죄송)

그 사용예는 앞으로 아주....자주 볼것이다.
그 중 DF만 한 번 보자.

위에서 살펴본 movsb 가 데이타를 옮긴후 esi, edi 값을 하나씩 증가 시킨다고 했다.
사실은, 이 DF 플래그 값에 따라 ...값을 증가시킬것인가 감소시킬것인가를 정한다.

즉, DF 가 0 이면 esi, edi 의 값을 증가 시키고,
DF 가 1 이면 esi, edi 의 값을 감소 시킨다.

cld 라는 명령어는 이 DF 플래그의 값을 0으로 세팅한다.
std 라는 명령도 있느데 이는 DF 플래그를 1로 세팅한다.




자...그럼.....위의 명령들을 한 번 확장해 보자.

movsb 라는 명령어에서 마지막 b는 byte 만큼씩 옮기라는 뜻이다.
그렇다면 movsw 도 있고, movsd도 있다.
여기서 w = word 를 d = double word 를 말한다.

실제 우리가 사용하는 memcpy 코드는 아래와 같다.
한 번 고민해 보면 좋지 않을까...싶다.

void memcpy ( void* dest, void* src, int num )
{
if (num <= 0) return;

_asm
{
mov?? edi, dest
mov?? eax, edi
mov?? esi, src
mov?? ecx, num
mov?? edx, ecx
shr?? ecx, 1
shr?? ecx, 1
cld
rep?? movsd
mov?? ecx, edx
and?? ecx, 0x03
rep?? movsb
}
}

///

오늘은.....저번에 배운 플래그 레지스터가 어떻게 활용되는지를
한 번 살펴 보자.

void?? strcpy (char* dest, char* src)
{
_asm
{
mov?? edi, src
mov?? ecx, 0xffffffff
xor?? al, al
cld
repnz scasb
not?? ecx

...
...
...
}

}

자...... 팔을 겉어 붙이고 한번 분석해 보자.

호호~~... 막 기분이 좋다....거의 다 아는 것들이 눈에 보인다.

xor al, al? (잉?.....) 왜 자신을 xor 했을까?

이 코드는 mov al, 0 과 같다....
근데.....위와 같은 코드를 레지스터의 값을 0으로 세팅할 때
아주....자주 사용하는 방법이다.
아니다.....거의 이렇게 사용한다.....
이유는 ?
아마도? mov al, 0 보다 속도가 빠르나 부다....
// 거의 이렇게 사용하는 코드
//-----------------------------------------------------------
레지스터 값을 0으로 세팅할때 xor 연산을 한다.

그리구,
아주 반가운 rep (repeat,반복하라),
근데, 이번에는 뒤에 nz 가 붙었다.
그 뜻은 (not zero)...이고, 이를 Repeat와 합해 보면,

repnz = Repeat While not zero?? ( 0 이 아닐 동안 반복하라 )
여기서 Not Zero 라는 것은
앞서 실행한 명령의 결과가 0 이 아닐때,
만약 실행한 결과가 0이면
플래그레지스터의 ZF(zero flag)가 1로 세팅되어 있을 것이다.
0 이 아닐때이므로
플래그레지스터의 ZF(zero flag)가 0일때라는 뜻이다.

rep는 반복할때마다 ecx 값을 하나씩 빼나간다....그러다 ecx = 0 이 되면
반복을 중단한다.
그러면 repnz 는 ecx = 0 일때도 중단하고, ZF = 1 일때도 중단한다.

카운트 만큼 반복하는 것은 C에서
for(int i=0;i<cnt;i++ ) { }

특정조건동안 반복하는 것은
while (a != 0) { }

뭐 이런 구문들을 생각나게 한다.

오~~~ 드디어 우리가 바야흐로....어셈으로 조건문을 배우기 시작하는가??
그 다음,
scasb 는 Scan String의 뜻으로

al - [edi] (edi가 가리키는 주소의 값)

연산한 결과를 가지고 플래그 레지스터의 값들을 바꾼고
edi 값을 하나 증가 시킨다.
물론, 그 결과값이 0 이면 ZF 플래그를 1 로 세팅한다.

바로 이놈이 반복을 멈추게 하는 놈이다.

즉, 조건절을 만들어 내는 부류의 명령이다.

// 이러한 조건문은
//----------------------------------------------------------------
1. 어떤 명령이....실행결과로써 플래그 레지스터를 건든다. (값을 바꾼다)
2. 뒤에서 이 플래그의 조건 (플래그의 값)에 따라 분기를 하거나 반복을 한다.

의 순을 따른다.

^_^
차차....더 알아 보자..

위 소스를 정리하면,
ecx에 0xffffffff값을 넣고,
al에 NULL을 넣고,
NULL을 찿을때까지...반복해서 ecx값을 빼 나간다.
NULL을 찿고 난뒤...ecx 값을 not 연산을 한다.
그러면, ecx에 src의 문자열에서 NULL까지의 문자수가 나온다.

ecx값을 return 하면 바로 strlen 함수가 된다.
그리구 ... 부분에 memcpy 기능을 붙여 넣으면 (ecx 만큼 memcpy를 하면)
바로 strcpy 함수가 완성되는 것이다.
여기서도.....비일비재한 코드를 하나 볼 수 있다.

// 비일비재한 코드
//------------------------------------------------------------------
ecx에 0xffffffff 값을 넣고 빼 나가다가 이를 not 연산하면 원하는 카운트를
얻는 코드.
strcpy 의 나머지 ... 부분은 여러분이 완성하기 바란다.

////

저번 시간에 조건문을 두드리기 시작했으니....
내친김에 조건문에 대해서 한 번 알아 보자.

for (int i=0; i<10; i++)
{
...
}

우리가 C 에서 보는 가장 흔한 문장 중에 하나이다.

for 문을 살펴보면,
1. 초기값 설정? ( int i=0; )
2. 조건부?????? ( i < 10; )
3. 반복부분???? ( i++; )

이렇게 나눌 수 있다.

먼저, 초기값 설정부문 (int i=0;) 은

mov? eax, i
xor? eax, eax

로 바꾸면 될 듯 하다.

다음 조건부 ( i < 10; )...
음. 복잡할 것 같으니 나중에 보자.

다음. 반복부분 (i++;)

inc? i

대충.. 합한 코드를 보면서 조건부분을 살펴보자.
mov eax, i????? // 초기값 세팅 부분 (i=0;)
xor eax,eax???? //
mov i,eax?????? //

loop_part:? <-- 이게 뭔지는 알겠지? (위치를 표시하는 라벨)

...
...
...

inc i?????????? // 반복부분? (i++;)

mov edx,i?????? // 조건부분? (i<10;)
cmp edx, 0x0a?? //
jl? loop_part?? //
cmp 라는 명령어가 보인다.
cmp = Compare (비교하라)
cmp edx, 10? ==? (edx - 10) 연산을 해서 플레그 값들을 세팅하라.
따라서 (edx-10)이 0보다 작으면 SF(Sign Flag,부호플래그)=1 이 될 것이다.

jl = Jump Less (작으면 분기하라)

cmp edx, 0x0a
jl? loop_part ==? edx 가 0x0a 보다 작으면 loop_part로 이동하라.

결국 loop_part 부분을 반복하게 될것이다.

여기서 조건관련 수식어들을 살펴 보자.

JE? = Jump Equal???? ( 같으면 분기하라)
JNE = Jump Not Equal (같지 않으면 ...)
JL? =????? Less????? ( 작으면 ... )
JNL =????? Not Less
JG? =????? Greater?? ( 크면 ... )
JS? =????? Sign????? ( (-)마이너스 이면 ... )
JZ? =????? Zero????? ( 0 이면... )
...
...

별거 아니다. (^_^)
=====================================================================

while 문도 한 번 살펴 보자....
While 문과 For 문과 비교해 보면 초기값설정부와 반복부분이 없다.
그 외는 아마도 동일할 것이다.

while (i>0)
{
...
...
}

이를 코드로 바꾸면...

loop_part: (라벨)
...
...
mov?? eax, i
test? eax,eax
jnle? loop_part

별루 설명할 것도 없지만,
test = Logical Compare
test eax,eax 는 eax 와 eax를 논리적 AND 연산하여 그 결과로 플래그들을 세팅한다.

근데, 문득 이런 생각이 든다.

jnle = 작지도 않고 같지도 않을때 JUMP 하라.
jg?? = 클때 JUMP 하라.

음.....같은거 아닐까???? ^_^

/////
저 번 시간 마지막 의문에 대한 정답은?? 예. 이다.

G? = NLE
LE = NG
NL = GE
NGE = L
PO = NP
P = PE
NBE = A
NA = BE
NZ = NE
E = Z
AE = NB
NAE = B

는 서로 같다...
자...

저 번 시간에 if 문과 while문을 살펴 보았다.
이 번 시간에는 switch 문을 한 번 살펴 보자.

c 로 된 아래와 같은 코드가 있다고 생각해 보자.

switch (x)
{
case 0:
printf? ("강태공 0");
break;
case 2:
printf ("강태공 2");
break;
case 3:
printf ("강태공 3");
break;
case 8:
printf ("강태공 8");
break;
default:
printf ("강태공 default");
break;
};
이를 어셈으로 바꾸면 아래와 같이 된다.

mov?? edx, x
sub?? edx, 0x01
jb??? case_0
dec?? edx
jz??? case_2
dec?? edx
jz??? case_3
sub?? edx,0x05
jz??? case_8
jmp?? case_default

case_0: (라벨)
...

case_2: (라벨)
...

case_3: (라벨)
...

case_8: (라벨)
...

case_default: (라벨)
...
^_^

설명하지 않아도 ....잘....알것이다.

switch 문이라서 별다를것 같지만, 막상 코드를 보면
별 다른것이 없다.

if/while/switch 문을 어셈으로 표현해 보면서
반대로 C 에서의 이들 구문의 특성을 알 수 있다.
요즈음과 같이 펜티엄 4가 대세인 시대에 이들 구문들을
속도때문에 구분해서 사용한다는 것은 별 의미가 없을 수 있다.
하지만, 혹시라도 속도를 고려해야할 때가 있다면
이제까지의 공부가 도움이 될 것이다.

---------------------------------------------------------------

본 강좌와는 무관하지만,
덧붙여서 지금까지 코드를 보면서 C 언어에 대해서 언급해 본다면,

어셈블리어가 기계어(CPU명령어)와 1:1 매치가 된다.
C 언어는 몇개의 CPU명령어를 조합해서 하나의 구문을 만든다.
결국, 어셈으로 구현할때 조금 손이 많이 가는 부분을 간단하게
표현할 수 있도록 한 정도로 볼 수 있다.

그래서, C 언어를 저급(보다 기계어에 가까운) 언어라고 한다.
상식적으로 생각해 봐도, 최적화만 잘해 준다면 거의 어셈으로
코딩한 것과 비슷할 것이다.
속도가 중요한 운영체제 같은 프로그램을 만드는데는
아주....딱.... 적합한 언어인 것이다.

반대로 생각해 본다면,
아주 마음에 안드는 언어이다.
도대체 자동으로 해 주는게 없다.
어셈으로 코딩하는것보다.....조금 나은 정도 인것이다.
아주 많은 로직과 방대한 분량의 코드를 작성해야하는
대형 시스템에게는 악몽과 같은 언어 이다.
혹시나 C 언어로 작업하는 대형 프로젝트가 있다면,
본 강좌를 읽고 있는 독자라면, 참여하지 말 것을 권하고 싶다.
요즈음 세상에는 사람을 죽이는 도구에? 칼이나 약만 있는것이 아니다.

이렇게 본다면,
향후 개발자가 되고 싶은 독자라면,
최소한 저급언어와 고급언어 하나씩은 익히고 있어야 할 것 같다.
고급언어는 기계의 관점보다 인간의 사고 관점에서
언어가 구성되어 있어서 훨씬 배우기 쉽우므로 크게 걱정하지
않아도 될 것이다.

---------------------------------------------------------------

이 쯤에서, 정리할 겸해서
CPU 명령들을 한 번 전체적으로 살펴 봐야할 것 같다.
필자가 아래에 리스트를 나열하면 독자들이 댓글로
그 명령들의 의미를 적어 주면 좋겠다.
1. 데이타 전송 명령들

CMOVE/CMOVZ
CMOVNE/CMOVNZ
CMOVA/CMOVNBE
CMOVAE/CMOVNB
CMOVB/CMOVNAE
CMOVBE/CMOVNA
CMOVG/CMOVNLE
CMOVGE/CMOVNL
CMOVL/CMOVNGE
CMOVLE/CMOVNG
CMOVC
CMOVNC
CMOVO
CMOVNO
CMOVS
CMOVNS
CMOVP/CMOVPE
CMOVNP/CMOVPO
XCHG
BSWAP
XADD
CMPXCHG
CMPXCHG8B
PUSH
POP
PUSHA/PUSHAD
POPA/POPAD
IN
OUT
CWD/CDQ
CBW/CWDE
MOVSX
MOVZX

^_^

별루 많지 않을 것이다.
//////

안녕하세요
하얀 고양이 입니다.
자주 쓰이고 눈에 거슬리는(?) 함수나 매크로등을 인라인 어셈으로 바꾸어 보자 라는 취지로 하게 되었습니다. 그 첫타는

ZeroMemory

이 함수 굳이 설명을 하지 않아도 memset과 자주 쓰이는 0으로의 초기화 함수중에 하나 이지요 :D 이것을 이번에 인라인 어셈으로 바꾸어 보고자 합니다.
이유인즉 아무리 CPU 속도가 좋아져도 역시 눈에 거슬리는 것은 거슬리는것
(ㅠㅠ)

push ebp
mov esp,ebp
..
..
pop ebp
ret

그리고 여기에 쓰이는 최소한의 스택 2개들 그리고
수를 알수 없는 cmp 명령어의 사용

위의 코드는 함수의 전형적인 코드 입니다.
이것들을 없에고 좀더 간단하게 코드를 줄여 보자!! 입니다.
아! 루프안이라면 아무래도 함수 보단 매크로 쪽이 좋을 듯 합니다..
(왜 인지는 한번 생각해 보심이 제 생각은 밑에 적겠습니다.)

우선 4의 배수의 크기를 가진 메모리를 초기화 하도록 하죠
타입을 2가지로 나누어야 합니다.
우선 포인터인것과 일반 적인 변수 형태 입니다.
코드 상에는 크게 차이는 없습니다. 그럼 적어 보도록 하죠
크기가 4의 배수 라면

xor eax,eax
mov ecx,크기
shr ecx,2
mov edi,주소 or lea edi,변수
rep stosd

이것 입니다.
코드를 설명 하면

xor eax,eax
eax에는 메모리에 넣어 줄 값입니다. 우리는 0으로 메모리를 채니까, 위처럼 했습니다. 사실 이 부분을 mov eax,0으로 해주어도 되지만 xor쪽이 조금 빠릅니다... :D

mov ecx,크기

이곳에는 크기를 넣어 주시면 됩니다. 직접 수를 적어 주셔도 되고 변수로도 가능합니다. 만약 변수의 이름이 length 라면
mov ecx,length 이런 식으로 해주셔도 됩니다.

shr ecx,2
이것은 4로 나누어 주기 위한 코드 입니다. 오른쪽으로 2번 쉬프트는.. 4로 나눈 효과이지요 :D 왜 인지는 밑에 설명이 갑니다...

mov edi,주소 or lea edi,변수

edi 에는 변수의 주소가 꼭 들어 가야 합니다. 즉 0으로 채워질 메모리 주소 입니다. mov는 뭔지 아시리라 보고 설명을 하지 않겠습니다, lea는 그 변수의 주소를 넣어 줍니다. 만약 메모리 변수 이름이 buff 라면(포인터 변수는 아닙니다.) lea edi,buff 로 해주시면 됩니다. 변수 타입에 따라 2개 중에 하나를 쓰시기 바랍니다.

rep stosd
가장 중요한 코드 인데..
굳이 풀자면 rep(eat) sto(re)s(tring)d(word)입니다.
반복해서(repeat) 저장해라(store) 문자열을(string) Dword로(Dword)..
어색하죠 ㅡㅡa
주 요는 edi에 eax를 ecx 만큼 저장 하라는 말입니다. Dword는 4바이트 입니다. 그러니까 eax전체가 edi의 주소로 복사가 됩니다.(eax는 4바이트이기 때문입니다.) 좀 부실 한것 같아서 재차 설명을 드리자면 eax는 4바이트 입니다
그런데 지금 위에서는 4바이트로 저장을 하죠? 그러니까.. eax전체가 저장이 되는 것입니다. 만약 eax가 0x00FF00FF라면 그대로 0x00FF00FF가 저장이 되는 것입니다.

xor eax,eax
mov ecx,크기
mov edi,주소 or lea edi,변수

이부분은

rep stosd

이 부분을 실행하기 위해 초기화 해준다고 생각 하시면 될 듯 합니다... ^^;;
자 이제는 나머지 4의 배수가 아닌 부분을 생각 해보죠. 이것도 보시면 아하 하실 정도로 간단 합니다.

mov eax,0
mov ecx,크기
mov edx,ecx
shr ecx,2
mov edi,주소 or lea edi,변수
rep stosd

mov ecx,edx
and ecx,03
rep stosb

입니다.
약간의 차이는 있지만 거의 비슷 하죠?
우선 추가된 코드를 보죠
mov edx,ecx 임시적으로 크기를 저장한것입니다.
잠시 후에 다시 설명을

mov ecx,edx
and ecx,03
이것은 4로 나눈 나머지를 구하는 것입니다.
어떻게 되는지는 생각 해보세요 :D(무책임 한가 ㅡㅡa)

rep stosb
이것을 또 풀죠
rep(eat) sto(re)s(tring)b(yte)입니다. 대충 무슨 말인지 알겠죠.
부실하지만 여기까지 입니다...
이것을 매크로로 만들어 쓰시면 괜찮을듯 합니다...

으음 위에서 했던 말인데.. 매크로와 함수의 차이를 아시리라 봅니다.
매크로는 사용한곳에 코드가 붙죠.. 그러나 함수는 그렇지 않고요.. 하지만
같은 것이 계속 반복 되는 것이라면 2개의 차이는 별 반 없습니다.
하 지만 매크로는 그대로 코드가 붙는 것입니다. 즉 함수 보다는 좀 더 코드가 없다는 것이지요 스택을 쓸 일도 없고.. +_+ ...하지만 이것도.. 3,4번 쓰일때 이지요.. 만약 수가 5개 10이상이라면 차라리 함수를 쓰길 권장하겠습니다......
각자의 장단 점이겠지요...
///////

안녕하세요.. 하얀 고양이 입니다.
이번엔 저번것을 확장판(?)을 해보려고 합니다..
이번엔 문자열 복사...
보통은 memcpy 같은 함수가 이것을 합니다.
물론 이것을 쓰셔도 되지만.. 앞에 서와 같은 이유(?) 로 인해.. :D
인라인으로 바꾸어 보도록 하죠..

mov ecx,복사 길이
shr ecx,2
lea edi, 대상
lea esi, 원본
rep movsd

간단 하죠... 확장이라고 한것이 부끄럽네요.. ( ..)

mov ecx,복사 길이
shr ecx,2

것은 아시리라 봅니다... 물론 4로 나누어 준것 입니다....
4바이트씩 복사가 되게 했으니 이렇게 설정을 해 주었습니다...
만약에 4바이트로 계산이 되어 있다면.. shr 명령은 없에 주셔도 되겠지요.

그리고.. 밑에 edi에는 주소 값만 들어 가면 됩니다. 만약 "대상"이라는 변수가
포인터라면 mov edi,대상 이런 식으로 해주셔야겠지요..
esi도 마찬가지로 edi와 같습니다 이것도 주소로...

그리고 많이 본
rep(eat) mov(e)s(tring)d(word)로 4바이트씩 복사를 합니다. 물론 ecx 수 만큼 복사를 수행 합니다.

만약 4의 배수가 아니라면...
전의 했던 방식으로 해주시면 됩니다. 다시 하자면..

mov ecx,복사 길이
mov edx,ecx
shr ecx,2
lea edi, 대상
lea esi, 원본
rep movsd
mov ecx,edx
and ecx,2
rep movsb
입니다.

+ Recent posts