IT_Programming/Assembly

[펌] 인라인 어셈블리를 분석하자!!

JJun ™ 2009. 10. 7. 17:28

출처 : http://securecode.tistory.com/34

 

하이텔 한동훈 님의 강좌 입니다. (한빛미디어 리눅스 커널프로그래밍 저자이시죠^^)

by blackjoe
------------------------------------------------------------------------------------
  강좌 :            인라인 어셈블리를 분석하자.
------------------------------------------------------------------------------------
     -- 부제 : /usr/src/linux/include/asm-i386/string.h 분석

이야기 꾼  : 한동훈  
인터넷 메일: ddoch@hitel.kol.co.kr, ddoch@nownuri.nowcom.co.kr
이야기 날짜: 1997년 2월 28일

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


1. 들어가는 말
GNU C( 리눅스의 gcc, 도스의 djgpp 등)의 인라인 어셈블리는 tasm 등의 문법과 조금 차이가 난다.

GNU C에서의 인라인 어셈블리, 외부 어블리어는 AT&T에 기반한 문법을 취함으로써 masm, tasm등의 INTEL문법과는 조금 차이가 나는 것이다. 자세한 문법상의 차이는 여러차례 번역하여 올려드린 AT&T 관련

어셈블리 HOWTO, GUIDE를 보시기 바란다.
일단 여기서는 리눅스 커널소스안에 위치한 "/usr/src/linux/include/ asm-i386/string.h"를 살펴볼 것이다. 인라인 어셈블리로 만들어 졌으며 우리가 익숙한 C 함수라 비교적 쉽게 이해가 갈 것이기 때문이다.
아주 감칠맛 나는 예제가 아닐 수 없다. :-)

GNU C의 인라인 어셈블리어는 다음과 같이 이루어져 있다.

asm("commands"
     : output
     : input
     : registers);

asm 대신에 __asm__ 키워드를 사용해도 되며, __volatile__ 키워드는 일단 신경 쓰지 마시기 바란다. __volatile__은 컴파일러로 하여금 해당 구문에 대해 함부로 자의적으로 수정,해석 하지 못하도록 하는

구실을 한다.

여기서의 분석은 자연스럽게 프로그램을 이해하기 위해서 input, registers, commands, output 의 순서를

취할 것이다. 그리고 설명의 편의를 위해서 AT&T 문법과 INTEL 문법을 적절히 혼용하겠다.

설명중 di/edi를 di나 edi로 표기하거나 si/esi를 si, esi로 대표하여 표기하는 경우가 종종 있다.

자, 이제 조금의 흥분되는 마음을 가라앉히고 여행을 떠나보자.

 

 


2. string.h 분석

 


2.1 strcpystrcpy는 C를 해본 사람에게는 정말 낮익은 것이다.

                   어떻게 내부적으로 어셈블리어로 표현될 수 있는 지 살펴보자.

extern inline char * strcpy(char * dest,const char *src)
{
     __asm__ __volatile__(
                          "cld\n"
                          "1:\tlodsb\n\t"
                          "stosb\n\t"
                                "testb %%al,%%al\n\t"
                          "jne 1b"
                          : /* no output */
                          :"S" (src),"D" (dest):"si","di","ax","memory");
     return dest;
}

* 먼저 input 부분을 보면
  movl src, %%esi
  movl dest, %%edi

  즉, src 포인터는 esi(source index)로 옮기며, dest포인터는 edi(dest index)로 옮긴다.

  항상 원래의 포인터는 esi/si로 옮겨지며 이동시키거나 작업할 대상 포인터는 edi/di로 옮기는 것을

  이 후에서도 자주 볼 수 있다.

* registers 부분은 si, di, ax 레지스터와 해당하는 메모리를 사용하므로 필요하면 컴파일러에게 해당하는

  레지스터/메모리의 값들이 손상되지 않도록, push/ pop작업을 하도록 지시하는 것이다.

  이렇게 함으로써 우리는 값들이 변질되는 것을 막기위하여 push/pop을 할 노고를 들 수 있다.

* cld 는 방향플래그(df)를 0으로 클리어하여 정방향 진행이 이루어진다.

----------------------------------------------------------------------
  플래그                        0으로 클리어               1로 세트
----------------------------------------------------------------------
캐리 플래그(cf)                   clc                            stc
방향 플래그(df)                   cld                            std
인터럽트플래그(if)                cli                            sti
----------------------------------------------------------------------

* lodsb/lodsw/lodsd     (Load String Byte/Word/Double)
  바이트/워드/더블워드의 자료를 esi가르키는 곳으로 부터 읽어와서 al/ax/eax 레지스터에 전송시킨다.

  전송된 후 esi는 다음번 문자열 요소를 가르키도록 갱신되는 데, df이 0이면 1/2/4만큼 증가되고,

  df가 1이면 1/2/4 만큼 감소된다. 즉, 여기서의 lodsb는 esi가 가르키는 곳으로부터 1바이트를 읽어와서
  al에 저장한다.

* stosb/stosw/stosd   (Store String Byte/Word/Double)
  al/ax/eax 레지스터의 값을 edi가 가르키는 곳에 복사한다. 복사후 edi의 값은 다음번 바이트/워드/

  더블워드를 가르키도록 갱신된다. 여기서의 stosb는 이미 읽어온 al의 값을 edi가 가르키는 곳으로

  복사를 한다.

* test     (Logical Compare)
  op1과 op2에 대해 비트 & 연산을 하여 그 결과에 따라 각 플래그의 값을 변경한다.

  op의 값이 변하지 않는다는 점을 제외하고는 and 명령과 동일하다.

 mov ah,  1010 0111b
  test ah, 1111 0000b
  --------------------
           1010 0000b

  test 의 결과는 1010 0000b(a0h)이다. 최상위 비트가 1이므로 sf(sign flag)가 1로 세트되고,

  a0h는 0이 아니므로 zf(zero flag)이 0으로 클리어된다. ah값은 변화가 생기지 않는다.

  zf가 1로 되려면 계산결과가 0이어야 한다.

  위의 예에서 testb %%al, %%al과 같이하면 항상 계산결과값은 자기자신(여기서는 al)이 된다.

  이해가 안되시는 분을 위해서..

  1010 0111
  1010 0111
  -----------
  1010 0111

  따라서 위의 testb문은 al의 최상위비트가 1이면 sf를 1로 세트하고 아니면 0으로 클리어 할 것이고,

  al의 값이 0이면 zf를 1로 세트할 것이다. 이 구문의 목적은 al이 0인지 알아보는 것이다.

* jne 와 일련의 opcodes  (Conditional Jump)
  현재의 각 플래그의 상태에 따라 실행을 label의 위치로 분기시킨다.

----------------------------------------------------------
  명령                   분기 조건
----------------------------------------------------------
jb / jnae / jc          cf = 1
jbe / jna               cf = 1 이거나 zf = 1
je / jz                 zf = 1
jecxz                   ecx = 0
jl / jnge               sf != of
jle / jng               sf != of 또는 zf = 1
jnb / jae / jnc         cf = 0
jnbe / ja               cf = 0 이며 zf = 0
jne / jnz               zf = 0
jnl / jge               sf = of
jnle / jg               zf = 0 이며 sf = of
jno                     of = 0
jnp / jpo               pf = 0
jns                     sf = 0
jo                      of = 0
jp / jpe      pf = 1
js                      sf = 1
----------------------------------------------------------
( less와 greater는 부호수치(Signed Number)인 경우의 비교,
  above와 below는 비부호수치(Unsigned Number)인 경우의 비교 )

위의 예에서 jne는 zf가 0일 경우, 즉 al이 0이 아닐 경우 해당위치로 분기한다. 라벨은 '1:'와 같이 적고,

후진참조일 경우는 'b', 전진참조 일 경우는 'f'를 분기하고자 하는 라벨뒤에 붙인다.

위의 예에서 esi(src)에서 1바이트를 읽어와서 먼저 edi(dest)에 적고 0이 아닐경우 계속복사작업을 반복하고 0일 경우 루프를 종료한다. 즉, 메모리에 대한 쓰기 작업이 레지스터를 통해서 마지막 널문자 '\0' 까지

복사를 하고 난 뒤에는 종료를 한다는 이야기이다.

어떤가? strcpy의 저급 행동양식이 눈에 들어오지 않는가?
위에서 설명한 opcodes들은 이후에도 줄기차게 나온다. 그럼, strncpy로 넘어가자.


 


2.2 strncpy

extern inline char * strncpy(char * dest,const char *src,size_t count)
{
     __asm__ __volatile__(
                     "cld\n"
                     "1:\tdecl %2\n\t"
                     "js 2f\n\t"
                     "lodsb\n\t"
                     "stosb\n\t"
                     "testb %%al,%%al\n\t"
                     "jne 1b\n\t"
                     "rep\n\t"
                     "stosb\n"
                     "2:"
                     : /* no output */
                          :"S" (src),"D" (dest),"c" (count):"si","di","ax","cx","memory");
     return dest;
}

* 먼저 input 필드를 보자.

  movl src, %%esi
  movl dest, %%edi
  movl count, %%ecx

  먼저와 달라진 점은 count 를 ecx에 옮긴 것 뿐이다.

* registers 필드의 si, di, ax, cx, memory는 직접적으로 사용을 한다는 것을 컴파일러에게 알려주어

  그 값을 보호하도록 한다.

* 이제 commands 필드를 하나씩 보자.
* 역시 cld로 방향플래그(df)를 0으로 클리어하였다.
* output, input의 피연산자들은 순서대로 %0, %1..로 commands필드에서 참조할 수 있다.

  위에서는, output은 없으므로 input 필드의 각각을 %0(esi), %1(%edi), %2(ecx)로 commands 필드에서

  참조할 수 있게 된다.

* dec / inc
  op의 값을 1만큼 감소/증가 시킨다. 감소 후의 결과에 따라 각 플래그의 값이 세팅된다.

  op는 비부호수치로 간주된다. cf는 dec의 영향을 받지 않는다. cf도 갱신하려면 subl $1, op 를 사용하면

  된다.

  위의 예, decl %2 는 ecx의 값을 1 감소시킨다.

* js 는 sf = 1 일 경우, 즉 ecx가 음수일 경우 해당 라벨로 건너뛴다.
* lodsb 로 esi가 가르키는 곳에서 1바이트를 가져와서 al로 옮긴다.
* stosb 는 al의 값을 edi가 가르키는 곳으로 1바이트를 옮긴다.
* testb %%al, %%al 로 al의 값이 0인지 검사한다.
* jne 는 위의 계산결과 값이 0이 아니면 라벨 1: 로 가서 루프를 반복한다.
* rep   (Repeat)
  문자열 처리 명령을 cx (cl/cx/ecx) 레지스터의 값만큼 반복 수행시킨다. 명령 종료후 cx는 0이 된다.

  cld
  movl $3, %%ecx
  rep
  movsb

  si가 가르키는 곳의 3바이트를 di가 가르키는 곳에 복사한다. 복사후 si와 di를 3만큼 증가하고

  cx는 0이 된다.

  ctd
  movl $3, %%ecx
  rep
  movsw

  si가 가르키는 곳의 3워드를 di가 가르키는 곳에 복사한다. 복사후 si와 di는 3*2 만큼 감소하고

  cx는 0이된다.

  위의 예에서의 rep, stosb는 al의 값을 di로 cx값만큼 반복하여 옮긴다.
  이 구문은 strncpy에서 src에서 dest로 반복회수 만큼 복사를 채 끝내기도 전에 0을 만났을 때에

  필요한 것이다. 즉 이때에는 남은 횟수만큼 al의 0을 di(dest)에 쓰게 된다.

* 요약을 하자면 각각의 아규먼트를 input 필드에서 esi, edi, ecx에 저장한 후 ecx가 0이하이면

  아무 작업도 하지 않고 js 2f로 인해 끝이 나고, 0이상이면 esi(src)가 가르키는 곳으로 부터 하나씩 al에

  가져와서 0이 아닐 동안 루프를 반복하여 복사한다. 만일 반복횟수 동안 src(esi)가 가르키는 곳에서

  널문자 '\0'이 나오지 않는 다면 dest(edi)에 널문자를 추가하지 않는 것을 알 수 있다.

  반복회수가 다 되지도 않았는 데 널문자가 나온다면 al은 0이 될 것이고 jne 1b를 통과하여 rep, stosb가

  실행이 되어서 al의 값 0이 나머지 남은 반복횟수 만큼 dest(edi)에 쓰여지는 것을 알 수 있다.
  몇가지 예를 들어 루프를 돌려보면 정확하게 동작함을 알 수 있다.

 



2. 3 strcat

extern inline char * strcat(char * dest,const char * src)
{
     __asm__ __volatile__(
                     "cld\n\t"
                     "repne\n\t"
                     "scasb\n\t"
                     "decl %1\n"
                     "1:\tlodsb\n\t"
                     "stosb\n\t"
                     "testb %%al,%%al\n\t"
                     "jne 1b"
                     : /* no output */
                     :"S" (src),"D" (dest),"a" (0),"c" (0xffffffff):"si","di","ax","cx");
     return dest;
}

* input 필드는 다음과 같다.

  movl src, %%esi           /* 원본 문자열이 있는 곳 */
  movl dest, %%edi         /* 복사할 대상 */
  movb $0, %%al             /* 찾을 문자 */
  movl $0xffffffff, %%ecx   /* 반복 횟수 */

  ecx에 왜 필요없을 것 같은 수치를 저장하는 걸까? 바로 위에서 dest(edi)에서 0을 찾는 데 필요한 횟수를

  대략적으로 잡아주는 것으로 쓰인다.

* 역시나 registers 필드에는 이 프로그램에서 사용되는 레지스터가 기술되어 있다.

* repne / repnz / repe / repz

  -- repne / repnz (Repeat while Nat Equal/Zero)
  문자열 처리 명령을 cx 레지스터의 값만큼 또는 zf이 1이 될때까지 (즉, 문자나 값이 서로 다를 경우-zf가

  0인 경우-에는 cx 값안에서 반복한다) 반복 실행시킨다. repnz과 repne명령은 서로 동일한 명령이다.

  명령 종료 후 cx의 값은 반복 실행된 횟수만큼 감소한다.

  movw $100, %%cx
  cld
  repne cmpsb

  si가 가르키는 1바이트와 di가 가르키는 1바이트를 비교하여 같지 않다면 si와 di를 1씩 증가시킨 후

  비교를 반복한다. 같은 바이트를 만나면 si와 di를 1증가시킨 후 비교를 종료한다.

  movw $100, %%cx
  cld
  repne scasw

  di가 가르키는 1워드와 ax의 값을 비교하여 같지 않는한 si와 di를 2씩 증가시킨 후 비교를 반복한다.

  ax의 값과 같은 워드를 만나면 si와 di를 2증가 시킨 후 비교를 종료한다.

  -- repe / repz (Repeat while Equal/Zero)
  문자열 처리 명령을 cx 레지스터의 값만큼 또는 zf의 값이 0일 때까지 반복 수행시킨다.

  repe와 repz는 서로 동일한 명령이다. 명령 종료 후 cx의 값은 반복 수행된 횟수만큼 감소한다.

  즉, 위와 반대되는 명령이다.

  cld
  movw $100, %%cx
  movb $0x20, %%al
  repe scasb

  si가 가르키는 1바이트가 20h(공백문자)일 경우 di를 계속 1씩 증가시켜 나간다.

  20h가 아닌 바이트를 만나면 di를 1증가 시킨 후 명령 실행을 종료한다.

  repe 명령 실행전 di가 가르키는 곳에 5개의 공란이 이어져 있다면 명령 실행 후 di는 6 증가하고

  cx는 6감소한다. scasb 대신 scasw를 사용했다면 1워드씩 ax의 값과 비교한다.

* 위의 구문에서 repne, scasb는 al의 0과 일치되는 문자를 edi(dest)가 가르키는 문자에서 찾아서

  찾은 0문자 다음을 edi가 가르키게 된다.

* decl %%edi 를 사용하여 한칸 앞의 0을 가르키도록 한다. 여기서부터 strcpy와 비슷하다.
* lodsb와 stosb를 사용하여 esi(src)에서 edi(dest : edi는 이제 널문자가 처음으로 나타난 위치를

  가리킨다.)로 한문자씩을 al을 경유하여 복사를 시작한다. 만일 esi(src)를 읽어 들이는 동안 널문자(0)이

  나타난 경우(al이 0인경우) zf가 1이 되므로 루프를 종료한다.

  이제, strncat을 살펴보자.

 



2.4 strncat

extern inline char * strncat(char * dest,const char * src,size_t count)
{
     __asm__ __volatile__(
                     "cld\n\t"
                     "repne\n\t"
                     "scasb\n\t"
                     "decl %1\n\t"
                     "movl %4,%3\n"
                     "1:\tdecl %3\n\t"
                     "js2f\n\t"
                     "lodsb\n\t"
                     "stosb\n\t"
                     "testb %%al,%%al\n\t"
                     "jne 1b\n"
                     "2:\txorl %2,%2\n\t"
                     "stosb"
                          : /* no output */
                     :"S" (src),"D" (dest),"a" (0),"c" (0xffffffff),"g" (count)
                     :"si","di","ax","cx","memory");
     return dest;
}

* input 필드는 count를 "g"로 저장하는 것만 빼고는 동일하다.
  여기서 "g"는 컴파일러에게 count의 값을 어디로 저장할 지를 일임하는 것이다.

  GNU C 컴파일러는 똑똑하기 때문에 최적화를 할 것이다.

  참고로 "r"은 컴파일러에게 어느 레지스터를 사용할 것인지를 일임하게 된다.

  commands 구문속에서 count는 %4로 참고된다.

* registers 필드는 이전과 비슷하며 output 필드도 필요치 않다.
* cld 는 역시 df를 0으로 클리어한다.
* repne, scasb 는 edi(dest)가 가르키는 문자가 al(0)의 문자와 같지 않는 동안 반복하고

  dest 중에서 0을 만난다면 0 다음 위치를 edi가 가르게 되고 검색을 종료한다.

* decl %%edi는 edi가 바로 이전의 0을 가르키게 한다.
* movl %4, %3 은 반복할 횟수 count의 값을 ecx에 저장한다.
* decl %3 은 decl %%ecx와 같으며 횟수를 하나 감소시킨다.
* js 2f 는 ecx의값이 음수라면 라벨 2로 간다는 것을 의미한다.

* lodsb, stosb, testb %%al, %%al, jne 1b는 각각 esi(src)로 부터 한바이트 씩의 값을 읽어와서

  al을 경유하여 edi(dest)로 복사를 하는데, al의 값이 0이 아니면 루프를 돌고 0이면 다음줄로 실행을 옮긴다.

* xorl %2, %2 는 xorl %%eax, %%eax 와 같다. xor는 두 비트가 서로 다르면 1이 되고 같으면 0이 되는

  배타적 논리합 연산자이다. xor는 비트단위로 수행되며 xor의 결과는 뒤의 피연산자에게 되돌려지며

  그 값에 따라 각 플래그의 값이 변경된다. 자신의 값으로 xor를 하면 당연히 0가 된다.
  xorl %%eax, %%eax는 0을 뒤의 %%eax에 저장을 시키고 그 값에 따라 플래그를 변경한다.

  stosb는 al의 값인 0을 edi(dest)가 가르키는 곳에 저장한다. 이로써 하나의 문자열을 완성하는 것이다.

* 요약하면, 먼저 cld, repne, scasb, decl %%edi 로 edi(dest)가 가르키는 곳에서 0을 찾아서 edi가

  그 위치를 가르키게 한다. movl %4, %3 과 decl %3, js 2f 는 count를 %%ecx에 저장하고 %%ecx를

  하나 감소시키면서 만일 카운트가 음 수일 경우 루프를 종료하여 edi(dest)가 가르키는 곳에 널문자를

  하나 적고 끝낸다. lodsb, stosb, testb %%al, %%al, jne 1b는 esi(src)부터 edi(dest)로 al을 경유하여

  0이 나올 때까지 복사를 하고 마지막에 stosb로 al의 0을 한번 더 edi(dest)가 가르키는 곳에 적어준다.

 


2.5 strcmp

바로 앞시간의 강좌물에서 수정해야 할 곳이 하나 있다. ^^;
strcat의 repe/repz 설명중 340 라인에서 첫부분 "si가 가르키는 ..."을 "di가 가르키는 "으로 수정한다.

먼저 들어가기 전에 소스구문 중에 붙어 있는 "\n\t"에 대해서 잠시 짚고 넘어가자.

현재 commands의 여러 라인들은 하나의 문자열에 불과하다. 따라서,

"cld"
"lodsb"
"jne"

등으로 사용할 경우 컴파일러는 "cldlodsbjne"의 문자열로 해석할 것이다. 따라서 제대로 해석하도록,

"cld
lodsb
jne"

와 같이 적어주거나 아래와 같이 각 라인마다 "\n\t"를 구분하여 적어주는 것이 좋다.

필자가 보기에는 "\n\t"와 같은 것은 gcc가 "\n"을 통해 어셈블리 명령들을 각각 구분하고 있는 것 같다.

이제 strcmp의 분석에 들어가보자.

extern inline int strcmp(const char * cs,const char * ct)
{
     register int __res;
     __asm__ __volatile__(
                     "cld\n"
                     "1:\tlodsb\n\t"
                          "scasb\n\t"
                          "jne 2f\n\t"
                          "testb %%al,%%al\n\t"
                     "jne 1b\n\t"
                     "xorl %%eax,%%eax\n\t"
                     "jmp 3f\n"
                     "2:\tsbbl %%eax,%%eax\n\t"
                     "orb $1,%%eax\n"
                     "3:"
                     :"=a" (__res):"S" (cs),"D" (ct):"si","di");
     return __res;
}


* 먼저 C에서 int로 __res로 하나 선언한다.

* 이번 소스에는 output필드가 있다.  commands에서 계산된 %%eax의 결과를 __res로 넘겨주는 것이다.    

  strcmp에서의 리턴값을 생각하면 된다. %%eax의 값은 C에서 기본적으로 리턴값으로 사용되기 때문에

  C에서 리턴형만 명시해 주면 굳이 __res 같은 것으로 output을 하지 않더라도 기본으로 리턴된다.

* input 필드는 다음과 같다.

  movl cs, esi
  movl ct, edi

  비교할 두개의 문자열을 가르키는 포인터의 값을 각각 source index와 dest index에 저장했다.

  원래는 메모리의 cs를 1(cs)와 같은 형식으로 참조해야 하지만 설명의 편의상 그냥 cs와 같이 적겠다.

* registers 필드를 보면 si와 di를 사용한다고 컴파일러에게 알려주고 있다.
  이번 프로그램에서 ax는 계산결과의 리턴 용도로 사용되기 때문에 이 필드에 포함을 시켜버리면

  아마도 제대로 된 값을 리턴하지 않을 것이다. ax는 C로 다시 제어권이 넘어올 때까지 그 값이 보관되어야

  하는 데 컴파일러에서 push, pop 을 하는 루틴을 집어넣어 버리면 어떤일이 생길까?

* commands 구문을 살펴보자.

* sbb    ( Subtract with Borrow )
  이번 strcmp에서 새롭게 나온 명령이다. 이것이 왜 필요한 지 알아보자.
  sbb는 op2(sbb의 두번째 인자)에서 op1+cf(캐리플래그)의 값을 뺀다. 결과는 op2에 되돌려진다.

  sbb명령은 복수바이트, 워드, 더블워드의 뺄셈에 사용된다. op1과 op2의 바이트수는 일치해야 하며

  sub명령과 동일하게 플래그들이 세트된다. 즉, 작은 수에서 큰 수를 빼면 자리수를 하나 빌려와야 하는데,  

  이 때 cf가 1로 세트된다. 따라서 다음에 sbbl %%eax, %%eax과 같은 계산을 하면 -1이 된다.

  subl %%eax, %%eax는 0이 되지만 sbbl을 같은 피연산자에 작동하면 자리넘김이 발생하는가의 여부에

  따라 (cf의 값에 따라) 0 이나 -1 이 되는 것이다.
 
  이제 처음부터 살펴보자.

* cld 로 df를 0으로 설정하고, lodsb로 esi(cs)가 가르키는 곳에서 1바이트를 al 로 가져온다.
* scasb 는 edi가 가르키는 값과 al의 값을 비교.검색한다. 같지 않다면(zf가 0이라면) 2f로 분기한다.

  비교시에는 al 에서 edi가 가르키는 값을 가상적으로 빼보는 데, edi가 더 크서 자리넘김이 발생한다면

  cf를 1로 세트한다. 따라서 2f에서의 sbbl %%eax, %%eax는 edi가 가르키는 값이 더 크다면 %%eax에는

  -1이 저장될 것이고 그렇지 않다면 0이 저장 될 것이다.

  orb $1, %%eax
  현재 %%eax는 0 (al(esi)가 가르키는 값이 더 클 경우)이거나 -1 (edi가 가르키는 값이 더 클 경우)인데

  여기에 1을 or연산을 해보자. -1일 경우에는 -1이 되고, 0일 경우에는 1이 된다. 한번 연습장에 적으면서

  검사해보자. 이로서 strcmp에서의 cs와 ct의 비교가 이루어져 크기가 cs가 크면 1이 , ct가 크면 -1이

  돌려짐을 알 수 있다.

* 문자열이 끝까지 같은 경우를 보자면, lodsb, scasb, testb, jne 1b를 거쳐 루프를 반복한다.

  그러다 마지막 널문자를 만나면 testb %%al, %%al로 al이 0인가를 검사해서 0이므로

  xorl %%eax, %%eax의 결과값은 0이 되어 %%eax에 저장이 되고, 3f로 분기해서 종료하게 된다.
 
* eax의 -1, 0, 1의 계산결과값은 output 필드에서 "=a" (__res) 항에 의해 __res에 되돌려진다.

* 요약하면, lodsb와 scasb로 cs이 가르키는 값을 옮긴 al의 값과 edi의 값을 비교하여 널문자가 나올 때까지

  같으면 xor연산으로 0을, al의 값이 더크면 or연산으로 1을, edi가 가르키는 값이 더 크면 -1을 돌려줌을

  알 수 있다. 

 



2.6  strncmp

extern inline int strncmp(const char * cs,const char * ct,size_t count)
{
     register int __res;
     __asm__ __volatile__(
                    "cld\n"
                    "1:\tdecl %3\n\t"
                    "js 2f\n\t"
                    "lodsb\n\t"
                    "scasb\n\t"
                    "jne 3f\n\t"
                    "testb %%al,%%al\n\t"
                    "jne 1b\n"
                    "2:\txorl %%eax,%%eax\n\t"
                    "jmp 4f\n"
                    "3:\tsbbl %%eax,%%eax\n\t"
                    "orb $1,%%al\n"
                    "4:"
                    :"=a" (__res):"S" (cs),"D" (ct),"c" (count):"si","di","cx");
     return __res;
}

* output 필드는 strcmp 때와 같이 eax의 계산결과 값을 __res로 되돌리고, input 필드에서는

  포인터 cs의 값은 esi에, 포인터 ct의 값은 edi에, count의 값은 ecx에 미리 저장을 한다.

  registers 필드에서도 앞과 마찬가지로 리턴값이 들어갈 eax를 제외한 변경되는 레지스터들이 적혀있다.

* 앞서도 이야기 했지만 output, input 이 commands에서 %0, %1 등으로 참조되는 참조되는 순서는

   output, input 순이다. 
 
* cld로 df를 0으로 클리어한다.
* decl %3은 참조순서에 따라 decl %%ecx와 똑같으며 검사할 카운트를 하나 감소시킨다.

  만일 음수이면 (sf-부호플래그-가 1이면) js 2f구문에 따라 2f로 분기한다. 라벨 2f에서는 eax를 xor연산으로

  0으로 만들고 4f로 분기하여 종료한다.

* decl에 의해 음수가 아닐 경우에는 esi(cs)가 가르키는 위치로부터 한바이트를 al로 옮겨 (lodsb) edi(ct)가

  가르키는 위치의 값과 가상적으로 빼봄으로써 같은가 검사를 한다. 같지 않다면 3f로 분기하여 sbbl구문과
  orb구문에 의해 al의 값이 더 크면 1이 edi(ct)가 가르키는 위치의 값이 더 크면 -1이 eax에 저장되어

  되돌려진다. strcmp에서의 orb $1, %%eax 보다는 orb %1, %%al의 구문이 정확해 보인다.

* 위의 scasb 로 al의 값과 edi(ct)의 값이 같다면 testb구문에 의해 al의 값이 0인지를 검사하여 0이 아니라면

   1b로 가서 루프를 돌고 0이라면 2: 에서 xor로 eax가 0으로 되고 4f로 분기하여 종료된다.

* eax의 값은 "=a" (__res) 구문에 의해 __res에 저장되어 C루틴으로 돌려진다.

* 요약하면, decl로 count를 먼저 1을 뺀다음에 lodsb, scasb로 cs와 ct 의 문자들을 비교를 하여

  같지 않다면 3f에서 -1, 1을 그 크기에 따라 반환하고, 같고 널문자를 만나면 xor로 eax가 0이 되어 반환되고,

  같고 널문자가 아니라면 루프를 돌다가 count가 음수가 되면 끝을 낸다.

 

 


2.7   strchr

C에서 strchr의 원형은 다음과 같다.

char *strchr(const char *s, int c);

문자열 s에서 문자 c가 처음으로 나타나는 곳의 위치를 돌려주는 것이다.
어셈블리 루틴을 살펴보자.

extern inline char * strchr(const char * s, int c)
{
     register char * __res;
     __asm__ __volatile__(
                                   "cld\n\t"
                                   "movb %%al,%%ah\n"
                                   "1:\tlodsb\n\t"
                                   "cmpb %%ah,%%al\n\t"
                                   "je 2f\n\t"
                                   "testb %%al,%%al\n\t"
                                   "jne 1b\n\t"
                                   "movl $1,%1\n"
                                   "2:\tmovl %1,%0\n\t"
                                   "decl %0"
                                   :"=a" (__res):"S" (s),"0" (c):"si");
     return __res;
}


* output 은 이전과 같다.
* input 에서는 esi에 s의 값을 저장하고, %0에 c를 저장한다. 여기서 "0"은 commands에서 %0으로

  참조할 수 있는 output 필드의 eax이다. 즉, eax(%0)은 입력값으로는 c가 저장되고 출력값으로는

  해당문자의 찾은 위치(포인터)가 저장된다. 마찬가지로 registers 필드에서는 si를 우리가 사용할 것임을

  알린다.

* movb %%al, %%ah 는 strchr에서 우리가 찾는 문자인 c가 al에 저장되어 있으므로

  ah로 백업을 한부 해둔다. 

* lodsb로 esi(s)가 가르키는 위치로 부터 한바이트를 읽어와서 al에 저장한다.
* cmpb %%ah, %%al 로 찾는 문자와 esi(s)가 가르키는 곳으로 부터 가져온 문자가 일치하는 지 검사한다.

  같다면 je 2f에 의해  movl %1, %0은 movl %%esi, %%eax와 같이 실행되어 찾은 다음의 위치를 eax

  저장한다. lodsb는 esi가 가르키는 곳에서 한바이트를 읽어오고 난 다음에는 esi를 하나 증가시키는 것을

  상기하자. 따라서 decl %%eax로 일치하는 문자가 있는 위치로 한칸 감소시켜야 한다.

* cmpb에서 ah와 al이 같지 않다면 testb 에 의해 al이 0인지 검사되고 0이 아니면 1b로 분기하여

  다시 비교루프를 반복한다. 그러다 못 찾고 널문자를 만나면 jne를 지나서 movl $1, %%esi로 esi에 1이
  저장되고 movl %1, %0 에 의해 esi의 값이 eax에 저장되고 decl %0 으로 eax가 하나 감소되어 0이 되고,

  이것은 그 유명한 NULL이 된다.

* 결과적으로 decl은 다목적 용도로 쓰이는 셈이다.


 


2.8  strrchr

C에서 strrchr의 원형은 다음과 같다. 

char *strrchr(const char *s, int c);

문자열 s에서 c문자가 마지막으로 나오는 위치를 돌려주는 것이다.
어셈블리 루틴을 살펴보자.

extern inline char * strrchr(const char * s, int c)
{
     register char * __res;
     __asm__ __volatile__(
                                        "cld\n\t"
                                        "movb %%al,%%ah\n"
                                        "1:\tlodsb\n\t"
                                        "cmpb %%ah,%%al\n\t"
                                        "jne 2f\n\t"
                                        "leal -1(%%esi),%0\n"
                                        "2:\ttestb %%al,%%al\n\t"
                                        "jne 1b"
                                        :"=d" (__res):"0" (0),"S" (s),"a" (c):"ax","si");
     return __res;
}


* output 필드를 먼저 보면 "=d" (__res)에 의해 edx 계산 결과값이 __res로 돌려짐을 알 수 있다.
* input필드에서는 output 필드의 첫번째 레지스터인 edx를 "0"으로 참조하고 있으며 그 edx에 0을 집어넣고

  있다. esi에 s를, eax에 c를 대입하고 있다. 마찬가지로 값을 되돌리는 output 레지스터를 제외한 변경되는

  레지스터를 마지막 필드 registers에 수록하고 있다.

* commands 필드에서는 먼저 al에 저장된 찾는 문자를 ah로 복사를 한 부 해두고 있다.

  (movb %%al, %%ah)

* 그 다음 lodsb로 esi(s)가 가르키는 곳의 값을 하나 가져와서 al에 수록한다.
* cmpb %%ah, %%al로 읽어온 문자와 찾는 문자가 같은 지 비교한다.
* 같지 않다면 2f로 분기하여 읽어온 문자(al)가 0이 아니라면 계속 검색해야 하므로 1b로 분기하여 루프를

  돈다. cmpb에 의해 같지 않아서 2f로 분기 했는데 testb에 의해 al이 0이면 edx의 값은 변경되지 않고

  0이 되어 결과적으로 NULL로 __res에 되돌려진다.

 

* 만일 cmpb에 의해 찾는 문자와 읽어온 문자가 같다면 leal -1(%%esi),%0가 수행된다.

  -1(%%esi)는 esi-1과 같다. lodsb는 한번 읽어오고 난뒤에는 esi를 하나 증가시키므로 같다고 판단될

  경우에는 esi는 벌써 다음위치를 가르키고 있으므로 leal -1(%%esi), %0은 leal -1(%%esi), %edx가 되어

  같은 문자가 있는 위치를 edx에 수록 한다. lea는 mov와 의미적으로는 비슷하다. 그런 다음 testb에 의

  al이 0인지 검사하고 0이면 문자열의 끝이므로 마지막으로 저장된 edx의 값을 __res에 되돌린다.

  만일 al이 0이 아닐 경우는 라벨 1b로 분기하여 불러와서 비교하기를 반복하고 같을 경우는 leal로 인해

  일치한 위치가 edx가 업데이트 된다. 이렇게 하여 일치한 문자의 최종 포인터가 edx에 저장되는 것이다.

* 아주 간결하게 잘 짜여져 있어서 제 역할을 다하고 있음을 알 수 있다.

 


2.9 strspn

strspn은 잘 사용해 보지 않았을 것이다. 원형은 다음과 같다.

size_t strspn(const char *cs, const char *ct);

이것은 ct에 없는 글자들 가운데 맨 처음으로 cs에 나타난 글자의 위치를 돌려준다. 이는 cs의 맨 처음부터

시작하여 ct에 있는 글자로만 이루어진 스트링의 최대 길이와 같다. ct의 맨 처음 글자가 cs에 없는 글자인

경우에는 0이 된다.

strspn("abcdefghijklmn", "abcdefg");

이것은 앞쪽 문자열 중 'g'까지의 갯수인 7을 반환한다.

이제 어셈블리 루틴을 살펴보자.

extern inline size_t strspn(const char * cs, const char * ct)
{
     register char * __res;
     __asm__ __volatile__(
                                   "cld\n\t"
                                   "movl %4,%%edi\n\t"
                                   "repne\n\t"
                                   "scasb\n\t"
                                   "notl %%ecx\n\t"
                                   "decl %%ecx\n\t"
                                   "movl %%ecx,%%edx\n"
                                   "1:\tlodsb\n\t"
                                   "testb %%al,%%al\n\t"
                                   "je 2f\n\t"
                                   "movl %4,%%edi\n\t"
                                   "movl %%edx,%%ecx\n\t"
                                   "repne\n\t"
                                   "scasb\n\t"
                                   "je 1b\n"
                                   "2:\tdecl %0"
                                   :"=S" (__res):"a" (0),"c" (0xffffffff),"0" (cs),"g" (ct)
                                   :"ax","cx","dx","di");
     return __res-cs;
}


* 조금 복잡해 보이지만 하나씩 살펴보자.
* output 은 "=S" (__res) 구문에 의해 esi 의 계산 결과값이 C 변수인 __res에 되돌려 짐을 알 수 있다. 

input 필드를 살펴보자.
* eax에는 0을 저장하고, ecx에서는 반복할 횟수로서 최대의 값을 저장하고 있으며, "0" (cs)는 esi에

  cs의 값을 저장함을 이야기 하고, "g"(ct) 는 ct의 값을 어디에 저장할 것인지를 컴파일러에게 맡긴다.


* registers 필드에서는 반환값이 저장될 esi를 제외한 레지스터가 수록 되어 있다.

commands 필드를 보자.
* movl %4, %%edi (movl ct, %%edi) 는 cs의 문자들을 검사할 ct의 값을 edi에 수록하고 있다.
* repne, scasb 는 al(값은 0)의 값과 edi가 가르키는 곳의 값을 비교하여 같지 않다면 반복하므로

  결국 ct에서 널문자가 있는 곳의 다음을 edi가 가르키게 된다.

 

* not
  not은 op1의 1의 보수값을 취하여 op1에 되돌린다. 즉 op1의 각 비트를 반전시킨 후, 그 결과를 op1에

  되돌린다. 플랙에는 아무런 영향도 미치지 않는다.

  위에서의 notl %%ecx는 ecx의 값을 비트반전 시킨다. 왜? 처음에 ecx의 값을 0xffffffff로 처기화 하였음을

  생각하자. 그리고 repne는 한번 실행 후 ecx의 값을 1감소시킴을 기억한다면, 이제 문자열 ct를 다음과 같이

  가정해보자.

ct : "abcdefg\0\0"  문자열 길이 = 7
        0123456 7 8  

  repne, scasb로 ct(edi)가 가르키는 곳에서 0을 찾을 때까지 반복한다면 7번위치에서 0을 찾고 edi를

  하나 증가시켜 8번위치를 가르키게 하고 repne를 종료한다. 각각의 위치를 edi가 가르킬 때의 ecx의 값은

  어떻게 변할까?

  0: 0xffffffff  1: 0xfffffffe  2: 0xfffffffd  3: 0xfffffffc
  4: 0xfffffffb  5: 0xfffffffa  6: 0xfffffff9  7: 0xfffffff8
  8: 0xfffffff7

  ecx :  0xfffffff7 ->   1111 1111 1111 1111 1111 1111 1111 0111
  notl %%ecx         ->   0000 0000 0000 0000 0000 0000 0000 1000

  notl %%ecx의 계산결과는 8이 된다. 즉, ct의 문자열 길이보다 하나가 길다.

* decl %%ecx 는 %%ecx를 하나 감소시켜서 ct의 문자열 길이만큼을 ecx에 취한다.

  이것은 cs의 하나의 문자를 ct 문자열과의 비교횟수로써 사용 된다.

* movl %%ecx, %%edx 는 ecx의 값을 edx에 백업한다. 

이제 준비작업을 끝내고 여기서 부터 본격 루프로 들어간다.
* 1:  lodsb 는 비교를 하기 위해서 esi로 부터 한바이트를 읽어와서 al에 위치 시킨다.

       testb 로 al의 값이 0인지 검사해서 (testb %%al, %%al) 0이면 문자열의 마지막이므로 2f로 분기한다

       (je 2f). 이때 분기 할 때 쯤이면 esi(cs)는 널문자 다음의 위치를 가르키게 된다. 그래서 2f에서는 esi를

       하나 감소시켜 cs내의 널문자를 가르키게 한다.

 

분기하지 않는 경우를 계속 보자.
* movl %4, %%edi는 edi에 ct의 값을 넣는다.
* movl %%edx, %%ecx 는 edx에 저장되있는 ct의 문자열 크기를 ecx에 저장한다.
* repne, scasb 는 ecx의 횟수만큼 현재 al에 올라와 있는 cs의 문자 하나와 edi(ct)가 가르키는 곳의 문자를

  차례대로 비교를 해서 같지 않다면 반복한다. 그래서 al의 같은 문자가 edi내에서 나오지 않는다면 zf는 0이

  될 것이고, 같은 문자가 나온다면 zf는 1로 세트될 것이다.


* je 1b는 cs의 문자하나가 ct의 문자열내에 있는 지 보아서 있다면 계속 1b루프로 가서 안나올 때까지나

  널문자가 나올 때까지 반복한다. 만일 같지 않다면 목적을 달성했으므로 하나가 더 커진 esi를 하나

  감소시키고 종료한다.

* 역시나 라벨 1b에서는 al로 문자를 하나 읽어들이고 널문자가 아니라면 edi로 ct의 값을 읽어들이고

  ecx의 값만큼 al과 ct를 비교한다.같다면 루프 1b로 가고 아니라면 종료한다.

  즉, esi의 값은 ct의 문자열속에 없는 문자의 다음을 가르키고 있음을 주목하자.

* __res에는 마지막 esi의 값이 담겨 있고 cs는 원 비교대상 문자열이 있으므로 __res - cs는 cs내의 ct와

  같지 않은 문자가 처음으로 나타나는 곳의 인덱스를 돌려준다.

 


2.10   strcspn

strcspn은 strspn과 반대의 역할을 한다. strcspn의 원형은 다음과 같다.

size_t strcspn(const char *cs, const char*ct);

이는 cs의 맨 처음부터 시작하여 str2에 없는 글자들의 연속적인 전체 수
와 같다.

extern inline size_t strcspn(const char * cs, const char * ct)
{
     register char * __res;
     __asm__ __volatile__(
                         "cld\n\t"
                         "movl %4,%%edi\n\t"       /* ct의 값을 edi에 옮긴다. */
                         "repne\n\t"                     /* al(0)의 값과 edi의 값을 반복비교하여 같은지 */
                           "scasb\n\t"                  /* 검색한다. 0문자를 만나면 그다음을 가르킨다. */
                         "notl %%ecx\n\t"             /* not 연산으로 ct의 문자열 갯수+1을 ecx에 구한다.*/
                         "decl %%ecx\n\t"            /* ct 의 문자열 갯수를 구한다. */
                         "movl %%ecx,%%edx\n"    /* ct의 문자열 갯수를 edx에 백업한다. */
                         "1:\tlodsb\n\t"                /* esi에서 한문자를 al로 가져온다. */
                         "testb %%al,%%al\n\t"      /* al이 0인가를 검사한다. */
                         "je 2f\n\t"                        /* 만일 0이라면 2f로 분기한다. */
                         "movl %4,%%edi\n\t"        /* ct의 값을 edi에 다시 옮긴다. */
                         "movl %%edx,%%ecx\n\t" /* ct의 문자열 갯수를 ecx에 다시 옮긴다. */
                         "repne\n\t"                      /* al 의 문자가 ct내에 나타나는 지를 ct의 문자열 */
                         "scasb\n\t"                     /* 갯수만큼 찾기를 반복한다. */
                         "jne 1b\n"                         /* al이 ct내에 없는 문자라면 1b로 분기하여 반복 */
                         "2:\tdecl %0"                     /* esi가 하나 더 증가되어 있으므로 하나 감소시킨다.*/
                         :"=S" (__res):"a" (0),"c" (0xffffffff),"0" (cs),"g" (ct)
                         :"ax","cx","dx","di");
     return __res-cs;       /* __res-cs는 cs내에서 ct에 없는 글자들의 전체수를 돌려주게 된다. */
}


* output 부분은 여전히 esi로서 __res에 전달된다.
* input부분은 여전히 eax 에는 0이, ecx에는 0xffffffff가, esi에는 cs가 전달되고 ct는 컴파일러에게 어디로

  값이 전달될 것인지를 맡긴다.

* registers 항목도 이전과 별 다를바 없다.

* commands필드를 하나씩 살펴보자. 반대로 작동하는 명령이 몇개 사용된 것을 제외하고는 strspn과

   별다름이 없음을 알 수 있다.


* strspn과 달라진 부분은 strspn의 je 1b가 여기서는 jne 1b로 바뀐 것 밖에 없다.

  즉, 이전에는 al이 ct내에 있으면 반복하던 것을 이제는 같지 않으면 반복하고 같으면 종료한다.

  주석을 참고하면서 한줄씩 따라가면 이해가 될 것이다.

 


2.11  strpbrk

strpbrk는 또 무엇인가? 원형은 다음과 같다.

char *strpbrk(const char *cs, const char *ct);

strpbrk는 strcspn과 같으나 처음으로 나타난 문자의 위치를 포인터로 넘겨 주는 것이 다르다.

어셈블리 루틴을 살펴보자.
strcspn과 거의 똑같고 마지막 한줄만 다르다.


extern inline char * strpbrk(const char * cs,const char * ct)
{
     register char * __res;
     __asm__ __volatile__(
                              "cld\n\t"
                              "movl %4,%%edi\n\t"       /* ct의 값을 edi에 옮긴다. */
                              "repne\n\t"                     /* al(0)의 값과 edi의 값을 반복비교하여 같은가를*/
                              "scasb\n\t"                    /* 검색한다. 0문자를 만다면 그다음을 가르킨다. */
                              "notl %%ecx\n\t"             /* not 연산으로 ct의 문자열 갯수+1을 ecx에 구한다.*/
                              "decl %%ecx\n\t"            /* ct의 문자열 갯수를 구한다. */
                              "movl %%ecx,%%edx\n"    /* ct의 문자열 갯수를 edx에 백업한다. */
                              "1:\tlodsb\n\t"                /* esi에서 한문자를 al로 가져온다. */
                              "testb %%al,%%al\n\t"      /* al이 0인지를 검사한다. */
                              "je 2f\n\t"                        /* 만일 0이면 널을 돌려주기 위해서 2f로 분기한다.*/
                              "movl %4,%%edi\n\t"        /* ct의 값을 edi에 다시 옮긴다. */
                              "movl %%edx,%%ecx\n\t" /* ct의 문자열 갯수를 ecx에 다시 옮긴다.*/
                              "repne\n\t"                      /* al의 문자가 ct에 나타나는 지를 ct의 */
                              "scasb\n\t"                      /* 갯수만큼 찾기를 반복한다. */
                              "jne 1b\n\t"                      /* al이 ct내에 없는 문자라면 1b로 분기하여 반복*/
                              "decl %0\n\t"                   /* ct에 나오는 문자를 찾았으므로 esi를 하나감소

                                                                          시킨다.*/
                              "jmp 3f\n"                         /* 종료한다. */
                              "2:\txorl %0,%0\n"            /* 널 포인터를 esi에 되돌린다. */
                              "3:"
                              :"=S" (__res):"a" (0),"c" (0xffffffff),"0" (cs),"g" (ct)
                              :"ax","cx","dx","di");
     return __res;
}


* 먼저 __res가 char *로 선언된 것에 주의하자. 그리고 esi는 역시나 __res로 값을 output 하고 있음을

  알 수있다. 마지막의 return __res는 포인터 그 차체를 리턴하고 있음에 유의하자.

* strcspn과 달라진 점은 다음과 같다.

     strpbrk                 strcspn

  decl %0                   2:  decl %0
  jmp 3f                
  2:  xorl %0, %0            
  3: (end...)

* xorl %0, %0은 esi를 0으로 만든다.(결과적으로 NULL 포인터로 만드는 것이다.)
* strcspn에서의 중간의 je 2f는 esi를 하나 감소시키고 종료했는데, strpb-rk에서는 ct에 해당하는 문자를

  cs내에서 못찾았을 경우는 끝까지 가서 널을 리턴하기 위하여 2f로 분기한다. xor는 널을 저장한다.


* ct에 나오는 문자를 cs에서 찾았을 경우는 esi-1 (decl %0)을 해서 직접 포인터를 넘겨준다.

 


2.12  strstr

strstr은 한번 씩 사용해 보았음직 하다.

char *strstr(const char *cs, const char *ct);

문자열 cs에서 ct가 처음으로 나타나는 곳의 포인터를 돌려준다.

extern inline char * strstr(const char * cs,const char * ct)
{
     register char * __res;
     __asm__ __volatile__(
                              "cld\n\t" \
                              "movl %4,%%edi\n\t"       /* ct의 값을 edi에 옮긴다. */
                              "repne\n\t"                     /* ct가 가르키는 곳에서 al(0)이 나올 때까지 */
                              "scasb\n\t"                    /* 검색을 반복한다. */
                              "notl %%ecx\n\t"             /* ct의 문자열 갯수 + 1을 ecx에 구한다. */   
                              "decl %%ecx\n\t"            /* ct의 문자열 갯수를 구한다.  */
                              "movl %%ecx,%%edx\n"    /* ct의 문자열 갯수를 edx에 백업한다. */
                              "1:\tmovl %4,%%edi\n\t"  /* ct를 edi에 다시 부른다. */
                              "movl %%esi,%%eax\n\t"  /* cs를eax에 백업한다. */
                              "movl %%edx,%%ecx\n\t" /* ct의 문자열 갯수를 ecx에 다시 부른다. */
                              "repe\n\t"                        /* cs와 ct의 문자를 ecx만큼 하나씩 */
                              "cmpsb\n\t"                    /* 하나씩 비교하기를 반복한다. */
                              "je 2f\n\t"                        /* 같다면 종료한다. 현재 eax에는 esi가 저장 */
                              "xchgl %%eax,%%esi\n\t" /* 같지 않다면 eax와 esi의 값들을 교환한다. */
                              "incl %%esi\n\t"               /* 다음비교를 위해서 esi를 하나 증가시킨다. */
                              "cmpb $0,-1(%%eax)\n\t" /* eax가 가르키는 곳의 바로 앞이 0인지 검사 */
                              "jne 1b\n\t"                      /* 0이 아니라면 비교를 반복한다. */
                              "xorl %%eax,%%eax\n\t"   /* 0이라면 eax에 널을 되돌린다. */
                              "2:"
                              :"=a" (__res):"0" (0),"c" (0xffffffff),"S" (cs),"g" (ct)
                                   :"cx","dx","di","si");
     return __res;
}


* commands 에서 값을 보존하기 위해서 저장과 교환이 자주 일어나는 데 이것만 자세히 보면 이해가

  갈 것이다.

* 먼저 output이 eax를 통해서 __res에 전달 된다는 점만 빼고는 input이나 registers 필드도 별 다를 게 없다.

* commands에서 xchg는 두개의 값을 교환하는 opcode이다. 이번에는 부분 부분씩 짤라서 보자.

  앞부분부터..

  cld
  movl %4, %%edi  /* ct -> edi */
  repne
  scasb
  notl %%ecx
  decl %%ecx
  movl %%ecx, %%edx

여기까지는 ct의 문자열 갯수를 ecx에 구해서 edx에 백업하는 과정으로
위에서 본 바와 같다.

1:  movl %4, %%edi            /* ct -> edi */
    movl %%esi, %%eax      /* esi(cs) -> eax */
    movl %%edx, %%ecx     /* esi와 edi를 비교를 반복할 횟수 */
    repe                             /* 비교 실행 */
    cmpsb
    je 2f
    xchgl %%eax, %%esi
    incl %%esi
    cmpb $0, -1(%%eax)
    jne 1b
    xorl %%eax, %%eax     /* 널을 만든다. */
2: (end..)

  ct(esi)의 값을 edi에 다시 저장하는 이유는 이전에 edi가 변경되었기 때문이다.

  esi는 이후의 비교루틴 속에서 변경이 되기 때문에 eax에 한번씩 비교를 하기 전에 백업을 해둔다.
  movl %%edx, %%ecx로 ct의 문자열 길이를 esi와 edi를 비교하기 위한 반복 횟수로 ecx에 옮긴다.

  이후에서 ecx는 계속 변경되기 때문에 edx가 그 값을 계속 보관하고 있다.
  repe cmpsb는 edi와 esi가 가르키는 곳의 문자를 서로 비교를 해서 같으면 zf를 1로 세트한다.
  je 2f는 문자열이 서로 같으면 끝을 낸다. 이때 eax에는 esi의 첫 포인터가 있으므로 정상적인 반환값이

  된다. esi는 세부비교 루틴속에서도 계속 변하고 있는 상태이므로 유동적인 값이 된다.
  xchgl %%eax, %%esi는 서로의 값을 교환한다. 즉, eax의 값을 esi에 넣는 것이 중요하다.

  eax에는 세부비교 이전의 포인터 임을 상기하자. incl %%esi는 cs가 가르키는 속에서 esi를 하나 증가시켜

  다음 문자를 가르키도록 한다. 여기서 중요한 것은 cs에서 널문자를 검색하는 과정이다.

  cmpb $0, -1(%%eax)

  여기서 eax는 xchg가 일어나기 전의 esi의 값이다. esi는 cs내에서 움직이므로 -1(%%eax)는 eax-1과

  같고 이것은 바로 전 세부비교 루틴에서 esi가 마지막으로 가르킨 값의 바로앞을 가르킨다.

  이것이 0이면 cs의 널문자를 가르키므로 찾지 못했다는 것이므로 jne 1b를 통과하여 xor를 사용하여

  eax에 0을 저장하여 NULL 포인터를 리턴한다. 만일 cmpb에서 0이 아니면 1b로 분기하여 edi, eax, ecx에

  각각 필요한 값들을 옮기고 저장하여 루프를 반복한다.

  즉, 1: 이후에서는 edi에는 ct가, eax는 ct내에서 한번의 외부비교를 하기 위해 ct내에서의 옵셋을 저장하는

  역할을 하고, edx는 세부 비교의 반복횟수의 백업용도로, ecx는 세부비교 횟수의 용도로 사용된다.
  그리고 중간에 eax와 esi의 값의 교환이 한번 일어난다. 반환은 eax를 사용한다. 매번의 eax의 움직임을

  잘 살펴보라.

  이제, 정확히 이해가 되는가?

  그럼, 다음으로 넘어가자.. :-)

 


2.13  strlen

아마 지금쯤 긴숨을 쉬는 분들이 많을 것이다. 잠시 쉬었다 하자.
strlen은 너무 간단하므로 담배를 한대 붙여서 피우면서 해도 그 담배가 다 타들어가기 전에 이해를 다 마치고

이번 시간을 마감할 것이다.

extern inline size_t strlen(const char * s)
{
     register int __res;
     __asm__ __volatile__(
                              "cld\n\t"
                              "repne\n\t"
                              "scasb\n\t"  /*  al(0)의 값과 edi가 가르키는 값을 비교한다. */
                              "notl %0\n\t" /* ecx에 s의 문자열 길이 + 1을 구한다. */
                              "decl %0"        /* s의 문자열 길이를 ecx에 구한다. */
                              :"=c" (__res):"D" (s),"a" (0),"0" (0xffffffff):"di");
     return __res;
}


* output 은 ecx를 통해서 __res에 전달되고, edi 에는 s를, eax에는 0이, ecx에는 0xffffffff가 전달된다.

  edi는 사용을 하므로 컴파일러에게 적절한 보관 지시를 내린다.

* 앞에서 많이 보아오던 부분이므로 쉽게 이해가 갈 것이다.
* input 필드에서 C에서의 변수값을 어떻게 적절히 해당 레지스터에 전달하는 지를 잘 보기 바란다.

(이것으로 이번시간은 마치겠다. 다음 시간에는 string.h에서 가장 분량이 많은 strtok를 살펴보겠다.

 미리 C루틴을 한번 생각해 본다면 이해가 빠를 것이다.  )

 

 


2.14  strtok

미리 숨을 크게 한번 쉬자. 그렇다고 해서 주눅들 필요는 없다. 분량만 많을 뿐이지 이전에서 보아왔던

루틴들의 지겨운 반복일 뿐이다. 차분히 토막토막 내어보자. 리눅서에게는 불가능이 없지 않은가?

이와 같은 C의 소스가 "/usr/src/linux/lib/string.c"에 있으니 복잡한 것은 비교를 해가면서 보는 것도

재미있을 것이다. 물론 이 둘 다 리누스 토발즈에 의해 쓰여졌다.

먼저 C에서의 strtok의 행동양식부터 파악하자.

char *strtok(char *s, const char *ct);

한마디로 strtok은 다른 함수들에 비해볼 때 비정상적인 것임은 틀림없다. 유용하기는 하지만.. ^^;

잠시 아래의 소스를 테스트해보자. 행동양식이 이해가 될 것이다.

---------------------------------------------------------------------
#include <stdio.h>
#include <string.h>

void main()

{
     char str[] = "ab:cd::ef";
     char *del = ":";
     char *token;
     token = strtok(str, del);
    

     while (token != NULL)

     {
          printf("%s\n", token);
          token = strtok(NULL, del);
     }
     printf("again = %s\n", str);
}

---------------------------------------------------------------------
결과는 다음과 같다.

ab
cd
ef

즉, 처음에 strtok(str, del)과 같이 호출하면 str에서 del에 해당하는 문자가 나올 경우 '\0'으로 채우고

처음으로 del에 해당되지 않는 문자에 대한 포인터를 리턴한다. 따라서

str : "ab\0cd::ef\0"
index: 012 3456789

와 같이 되어 있을 것이다. index 0의 포인터를 돌려주고 아마도 index 3의 포인터는 ___strtok 전역변수에

저장할 것이다. 그 다음에

token = strtok(NULL, del);

와 같이 호출하여 str대신에 NULL이 주어진다면 이전에 저장된 ___strtok 변수를 사용할것이다.

이번에는 index 5를 '\0'로 채우고 index 3의 포인터를 반환할 것이고 index 6에 대한 포인터는 다시

___strtok에 저장될 것이다. strtok(NULL, del)을 한번 더 호출 한다면 index 6에서 del에 해당하는 문자는

건너뛰고 index 7에 대한 포인터를 돌려줄 것이고,  더이상 del 문자가 보이지 않고 '\0'을 만나므로 ___strtok

에는 NULL을 저장할 것이다. 한번 더 위와 같이 호출한다면 이제는 더 이상 분리할 토큰이 없으므로 ___strtok

의 NULL을 리턴값으로 돌려 줄 것이다. 만일 strtok(str2, del)과 같이 다른 문자열에 대한 작업을 한다면

___strtok 변수의 값이 더이상 이전의 문자열 포인터에 대한 정보를 가지지 않고 새롭게 처음부터 시작할

것이다.

"/usr/src/linux/lib/string.c"의 strtok의 C 소스를 잠깐 보자.

제일 앞쪽에 ___strtok 이 전역변수로 선언되어 있다.
---------------------------------------------------------------------
/* strtok --  /usr/src/linux/lib/string.c 에서.. */

char * ___strtok = NULL;

char *strtok(char *s, const char *ct) {
char *sbegin, *send;

/* s의 값이 NULL(0)이면 이전에 저장된 ___strtok 의 값이 sbegin에 저장되고 아니면 s의 값이 sbegin으로

    저장되어 새로운 str(s)에 대해서 작업을 한다. */
sbegin = s ? s : ___strtok;
                       
/* sbegin이 NULL(0)이라는 것은 ___strtok이 NULL(0)이라는 것을 의미하며 더이상 분리할 토큰이 없다는

    것을 의미한다. NULL을 리턴한다. */
if (!sbegin) {      
return NULL;
}

/* sbegin은 이제 ct와 함께 strspn으로 호출된다. strspn 의 역할은 sbegin에서 ct에 없는 문자가 처음으로

    나타나  나는 곳의 위치를 돌려준다. 이제 sbegin은 처음으로 ct 에 속하지 않는 sbegin내에 위치를

    가르킨다. 위의 예를 들면

   "ab:cd::ef\0"   : ct = ":"
    0123456789

   에서 sbegin은 index 0을 가르킬 것이다.  */
sbegin += strspn(sbegin, ct); /* strspn 함수 사용 */

/* 만일 sbegin이 가르키는 문자가 널이라면 더 이상 분리할 토큰이 없으므로 ___strtok을 NULL로 세팅하고

    NULL을 리턴한다.            */
if (*sbegin == '\0') {
___strtok = NULL;
return (NULL);
}

/* strpbrk는 strspn과는 거꾸로 동작한다. 즉, sbegin에서 부터 시작하여 ct에 해당하는 문자가 나오는

    첫 위치를 포인터로 돌려준다고 했다. 앞서의 예를 들면,

    "ab:cd::ef\0"   : ct = ":"
    0123456789

   strpbrk를 호출하면 현재 sbegin은 index 0을 가르키고 있으므로 그 리턴 포인터는 ct(":")가 처음으로

   나오는 index 2 에 대한 포인터를 send로 넘겨줄 것이다.    */

send = strpbrk( sbegin, ct); /* strpkrk 함수 사용 */

/* send가 널포인터가 아니고 send가 널문자를 가르키지 않는다면 send가 가르키고 있는 index 2는

    '\0'문자로 되고 send의 값은 이후의 호출을 위해서 1이 증가하여 ___strtok에 저장을 한다. sbegin은

    현재 어디를 가르키고 있는가? 마지막으로 sbegin이 사용된 곳은 바로 위의 index 0을 가르키고 있을

    때이다. 따라서 index 0에 대한 포인터를 최종적으로 리턴 한다. ___strtok은 현재 index 3에 대한 포인터를

    보유하고 있다. */

if (send && *send != '\0')
*send++ = '\0';
___strtok = send;


return (sbegin);
}
---------------------------------------------------------------------

이제 정확히 이해가 되는가? 나머지의 문자열에 대해서도 함수를 돌려 보라.

위의 프로그램의 흐름과 같은 간단히 그림을 그려보겠다.
어셈블리 루틴에서 참고가 될 것이다.

[그림1]
----------------------------------------------------------------------
           0이 아님
s        ------------->   sbegin = s
         ------------->   sbegin = ___strtok
               0 임

               0 임
*sbegin  ------------>    ___strtok = 0, return 0

strpbrk(sbegin, ct) ----->  send 

                          0이 아님
send && *send    ------->  *send++ = 0, ___strtok = send, return sbegin
-----------------------------------------------------------------------

이렇게 장황하게 설명하는 것은 어셈블리 루틴에서의 부하를 조금이라도 줄이고자 하는 마음에서이다.

이제 어셈블리 루틴을 대략적으로 살펴보자.

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

extern inline char * strtok(char * s,const char * ct)
{
     register char * __res;
     __asm__ __volatile__(
                                   "testl %1,%1\n\t"
                                   "jne 1f\n\t"
                                   "testl %0,%0\n\t"
                                   "je 8f\n\t"
                                   "movl %0,%1\n"
                                   "1:\txorl %0,%0\n\t"
                                   "movl $-1,%%ecx\n\t"
                                   "xorl %%eax,%%eax\n\t"
                                   "cld\n\t"
                                   "movl %4,%%edi\n\t"
                                   "repne\n\t"
                                   "scasb\n\t"
                                   "notl %%ecx\n\t"
                                   "decl %%ecx\n\t"
                                   "je 7f\n\t" /* empty delimiter-string */
                                   "movl %%ecx,%%edx\n"
                                   "2:\tlodsb\n\t"
                                   "testb %%al,%%al\n\t"
                                   "je 7f\n\t"
                                   "movl %4,%%edi\n\t"
                                   "movl %%edx,%%ecx\n\t"
                                   "repne\n\t"
                                   "scasb\n\t"
                                   "je 2b\n\t"
                                   "decl %1\n\t"
                                   "cmpb $0,(%1)\n\t"
                                   "je 7f\n\t"
                                   "movl %1,%0\n"
                                   "3:\tlodsb\n\t"
                                   "testb %%al,%%al\n\t"
                                   "je 5f\n\t"
                                   "movl %4,%%edi\n\t"
                                   "movl %%edx,%%ecx\n\t"
                                   "repne\n\t"
                                   "scasb\n\t"
                                   "jne 3b\n\t"
                                   "decl %1\n\t"
                                   "cmpb $0,(%1)\n\t"
                                   "je 5f\n\t"
                                   "movb $0,(%1)\n\t"
                                   "incl %1\n\t"
                                   "jmp 6f\n"
                                   "5:\txorl %1,%1\n"
                                   "6:\tcmpb $0,(%0)\n\t"
                                   "jne 7f\n\t"
                                   "xorl %0,%0\n"
                                   "7:\ttestl %0,%0\n\t"
                                   "jne 8f\n\t"
                                   "movl %0,%1\n"
                                   "8:"
                                   :"=b" (__res),"=S" (___strtok)
                                   :"0" (___strtok),"1" (s),"g" (ct)
                                   :"ax","cx","dx","di","memory");
     return __res;
}

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

*output 
  ebx를 통하여 __res로 값을 하나 전달하고,  (%0)
  esi를 통하여 ___strtok 전역변수에 값을 전달한다. (%1)

* input 
  ebx를 통하여 ___strtok의 값을 전달하고,   -- %0
  esi를 통하여 s의 값을 전달하며,           -- %1
  ct의 값의 전달은 컴파일러에게 맡긴다.   (%4) 

  여기에서 ___strtok은 ebx로 값이 전달되지만 나중에는 esi의 계산결과가 ___strtok으로 저장된다.

  ebx에서는 __res가 나온다. 입력과 출력이 서로 엇갈리므로 잘 살펴봐야 한다. 프로그램에서는

  %0, %1, %4로 각각 ebx와 esi,ct가 참조되고 있다. ct의 값이 전달된 곳은 output, input 의 순서에 따라

  %2가 아니고 %4이다.

* registers 
  리턴하는 레지스터 ebx, esi를 제외한 "ax", "cx", "dx", "di", "memory" 를 보호하고 있다.

* commands
  먼저 C에서 char * __res를 하나 선언하고 있다. ___strtok은 전역변수로 이미 존재해야 한다.

  라벨단위로 짤라서 집중적으로 살펴보자.

/* if (s != NULL) goto 1;
   else if (___strtok == NULL) goto 8(return NULL);
   else __res = ___strtok;
  
   # s 가 NULL이 아니면 새로운 문자열에 대한작업이 시작되므로 1: 로 간다(testl %1, %1; jne 1f).
   # 만일 s가 NULL일 경우는 이미 전단계에서 작업이 진행된 상태로 보므로 ebx로 전달된 ___strtok의 값이

      NULL이면 더이상 분해할 토큰이  없다는 이야기가 된다. 따라서 종료한다(testl %0, %0; je 8f).
      종료시에는 결과값으로 현재 ebx (원래의 ___strtok의 값: NULL)이 __res로 리턴되고,

      s가 NULL 인 상태이고, 이것이 esi에 전달되었으므로 ___strtok에는 NULL이 저장된다. (output 필드)
  

   # s 가 NULL이고 ___strtok이 NULL이 아니라면 현재 작업이 진행중이므로 esi(source index)로 옮긴다.

      esi는 원래 작업처로 쓰인다
      (movl %0, %1).   */

"testl %1,%1\n\t"   /* testl %%esi(s), %%esi     */
"jne 1f\n\t"         
"testl %0,%0\n\t"   /* testl %%ebx(___strtok), %%ebx  */
"je 8f\n\t"        
"movl %0,%1\n"      /* movl %%ebx(___strtok), %%esi */

/* __res = 0;  그리고 ct의 문자열의 길이를 구하는 부분이다. */

# ebx를 0으로 만든다. (xorl %0, %0)
# ct의 문자열의 길이를 구하기 위한 검사반복횟수를 0xffffffff로 하여 ecx에저장한다. (movl $-1, %%ecx)
# eax를 0으로 만든다. (xorl %%eax, %%eax)
# ct의 길이를 구하기 위해 ct를 dest index로 옮긴다. (movl %4,%%edi)
# repne, scasb로 al(현재 0)의 값이 edi가 가르키는 곳에 있는 값이 같은 지를 ecx만큼 반복한다.

  edi의 값이 변경된다.
# notl, decl로 ecx에 ct의 길이를 구한다.
# 만일 ecx가 0이면 ct가 ""이 되므로 7로 간다. (je 7f)  

# 세부비교횟수인 ecx를 안전한 edx에 보관한다. (movl %%ecx,%%edx)

"1:\txorl %0,%0\n\t"        /* xorl %%ebx, %%ebx */
"movl $-1,%%ecx\n\t"      /* 검사반복 횟수 0xffffffff -> ecx */
"xorl %%eax,%%eax\n\t"
"cld\n\t"
"movl %4,%%edi\n\t"       /*  movl (ct), %%edi */
"repne\n\t"
"scasb\n\t"       
"notl %%ecx\n\t"
"decl %%ecx\n\t"  
"je 7f\n\t"                      /*   if (strlen(ct) == 0) goto 7;  */
"movl %%ecx,%%edx\n"

/* __res  += strspn(__res, ct);
   strspn루틴이다. al이 edi속에 포함되는가를 살펴서 포함된다면 루프를 반복하여 처음으로 edi(ct)속에

   포함되지 않는 곳을 구한다. s문자열을 al로 끌어올리는 도중에 0이 발견된다면 문자열의 끝이므로 7로

   간다. 이 이야기는 s내에서 토큰 분리자를 만나고 있는 가운데 '\0'을 만난다는 이야기다.

   현재 __res = 0이다. 따라서 7에서 NULL을 저장한다. 계산된 esi의 값을 ebx로 저장하는 부분을

   눈여겨 봐두자. 이것은 output 에서 __res로 리턴될 값이다. */

"2:\tlodsb\n\t"                   /* al에 esi(s)에서 하나의 문자를 가져온다. */
"testb %%al,%%al\n\t"         /* al 이 0이면 7 로 */
"je 7f\n\t"           
"movl %4,%%edi\n\t"           /* edi가 변경되어서 다시 가져옴 ct ==>  edi */
"movl %%edx,%%ecx\n\t"    /* 세부비교에서 반복횟수를 가져옴 */
"repne\n\t"                         /* al(0)의 값과 edi가 가르키는 곳의 값이 */
"scasb\n\t"                        /* 같을 때까지 비교반복 */
"je 2b\n\t"                          /* 같다면 2로 */
"decl %1\n\t"                      /* 다르다면 esi(s)를 하나 감소 */
"cmpb $0,(%1)\n\t"             /* *esi(*s)에서 널이 나온다면 7로 */
"je 7f\n\t"           
"movl %1,%0\n"                   /* esi -> ebx */


/* send = strpbrk( __res, ct);
   if (send == NULL) goto 5;   /* testb %%al, %%al; je 5f  */
   if (*send == '\0') goto 5;
   else *send++ = '\0';

   여기는 strpbrk 역할을 한다. 위에서는 del(또는 ct)에 해당하지 않는 문
   자("ab:cd::ef"중 'a'과 같은 것)를 찾았지만 이제는 토큰분리자로 쓰이
   는 del(ct -- 예에서는 ":")를 s내에서 찾는다. 따라서 s내에서 0을 만
   나더라도 __res는 NULL이 되지 않고 ___strtok만 NULL이 된다. */

"3:\tlodsb\n\t"                 /* esi에서 하나불러서 al 로 */
"testb %%al,%%al\n\t"       /* esi의 현재문자가 0이면 5 로 */
"je 5f\n\t"
"movl %4,%%edi\n\t"         /* edi가 변경되어서 다시 가져옴 ct ==> edi */
"movl %%edx,%%ecx\n\t"  /* 반복횟수를 가져옴 */
"repne\n\t"                       /* al 의 문자와 edi의 문자를 횟수만큼 비교 */
"scasb\n\t"
"jne 3b\n\t"                      /* s의 현재문자가 ct에 나오지 않는다면 3으로*/
"decl %1\n\t"                   /* 나왔다면 esi를 하나감소 */
"cmpb $0,(%1)\n\t"           /* *esi가 0인가를 검사 */
"je 5f\n\t"                         /* 널이면 5로 */
"movb $0,(%1)\n\t"           /* 0을 *esi(*s)에 추가  */
"incl %1\n\t"                    /* esi를 하나증가시키고 6으로 */
"jmp 6f\n"  


/* ___strtok = NULL */
"5:\txorl %1,%1\n"     /* esi를 0으로 만듬 */


/* if ( *send != '\0') goto 7;
   if ( *send == '\0') ___strtok = NULL;   */

"6:\tcmpb $0,(%0)\n\t" /* *ebx가 0인지 검사해서 아니면 7로 */
"jne 7f\n\t"
"xorl %0,%0\n"              /* *ebx가 0이면 ebx를  0으로 만듬 */

/* if (__res != 0) goto 8;
   else ___strtok = __res; */

"7:\ttestl %0,%0\n\t"   /* ebx가 0인지 검사해서 아니라면 8로 */
"jne 8f\n\t"
"movl %0,%1\n"           /* ebx 를 esi로 */

/* return __res;   */
"8:"


:"=b" (__res),"=S" (___strtok)
:"0" (___strtok),"1" (s),"g" (ct)
:"ax","cx","dx","di","memory");


return __res;
}
------------------------------------------------------------------------------------

상당히 복잡함을 느낄 것이다. 하지만 중간 중간 라벨 단위로 해당하는 C의 형태를 기술해 두었으므로

비교적 이해가 될 것이다. 라벨 1번 이전에는 ebx, esi가 input과 output 이 혼재하는 상황이므로
잘 연관시켜야 한다. 1번이후부터는 ebx는 __res와 esi는 ___strtok과 연관을 시켜서 유심히 살펴보자.

같은 소스이지만 C로 하는 것보다 어셈블리어로 기술하면 라벨로의 점프 때문에 얼마나 혼잡스러운지

납득이 갈 것이다.

이해가 잘 되는가? 나름대로 이해를 하셨다면 여러분도 이제 AT&T인 라인 어셈블리에 어느정도 익숙해졌다고 볼 수 있으며 간단한 루틴은 자신의 소스에 포함을 시킬 수 있는 능력을 가졌다고 봐도 과언이 아닐것이다.

아울러 커널 소스 같은 곳에서 나오는 인라인 어셈블리도 잘 납득이 될 것이다. 보호모드만 제외한다면..

이번 시간에는 strtok 하나만으로 400라인을 차지한 것 같다. 마의 strtok을 이해하신 분에게 힘찬 박수를

보내고 싶다. ^^; 고수의 입장에서 보면 언제나 쉬운 문제겠지만 우리같은 초보에게는 힘들 따름이다.

여러분들이 커널소스나 어셈블리 관련 소스를 공부할 기회가 있다하더라도 한 모듈이 이 만큼 많이

나오는 것은 보기가 힘들 것으로 믿는다. 아마 필자도 이말을 제번벅 할 일이 있을 지도 모르겠지만..



2.15 memcpy

이제 부터는 작은 것 몇 개만 보면 끝날 것이다. 푸근히 마음을 가지고 여유있게 살펴보도록 하자.

길어봐야 10줄 짜리들이다.

---------------------------------------------------------------------
extern inline void * __memcpy(void * to, const void * from, size_t n)
{
     __asm__ __volatile__(
                                    "cld\n\t"
                                    "rep ; movsl\n\t"
                                    "testb $2,%b1\n\t"
                                    "je 1f\n\t"
                                    "movsw\n"
                                    "1:\ttestb $1,%b1\n\t"
                                    "je 2f\n\t"
                                    "movsb\n"
                                    "2:"
                                    : /* no output */
                                    :"c" (n/4), "q" (n),"D" ((long) to),"S" ((long) from)
                                    : "cx","di","si","memory");
     return (to);
}
----------------------------------------------------------------------

memcpy 는 잘 아시다시피 from에서 to로 n바이트를 메모리 복사한다.

*  output
   memory 복사 이므로 반환값은 없다.

*  input  
   "q"는 ax, bx, cx, dx 중 하나의 레지스터에 'n'의 값을 할당하겠다고 컴파일러에게 일러주는 것이다.
   ecx에 n/4를, "q"에 n을, edi에 (long)to를, esi에 (long)from을 각기 값을 전달한다.

   (long)과 같이 특정하게 형을 변환함으로써 그 크기에 따른 레지스터(여기에서는 edi등..)을 사용할 수

   있다. to는 복사될 곳이므로 dest index에 넣어두고, from은 복사할 곳이므로 source index에 값을

   저장하여 이후에 문자열 관련 명령을 사용하여 원하는 작업을 할 수 있다.

* commands
   rep; movsl과 같이 명령들을 ';'로 분리하여 한줄에 적을 수도 있다.
   이 명령에 의해 esi(from)에서 edi(to)로 n/4만큼 복사된다. 한번 복사될 때는 4바이트(movsl-long)씩

   되므로 만일 n을 15로 줄 경우는 4의 배수인 12만큼 복사되고 3바이트가 남게 된다.

   이렇게 4바이트 단위로 복사하는 것이 4바이트가 기본 데이타 형인 32비트 운영체제에서 더 효율성이

   좋은 모양이다. 나머지 3바이트는 뒤에서 복사를 한다.
   중간에 %b1(%bl-%BL과 혼동하지 말라)이 있는 데 이것은 아마도 %1+byte 의 의미같다.

   즉, %1은 n의 값이므로 testb $2, %b1은 n의 하위 1바이트와 상수 2를 & 연산을 한다는 이야기로 해석하면

   정확할 것 같다. 15바이트 일 경우를 예로 들면, 

  15 :   1111
   2  :   0010
   &  -----------
          0010
   즉, 이 연산은 n의 2번째 비트가 1인지를 검사한다. 0이라면 1첫번째 비트를 검사하는 1f로 가고,

   1이라면 2비트를 복사를 하고 1번째 비트를 testb $1, %b1과 같이 검사하여 1이면 1바이트를 더 복사하고,

   0이면 종료한다.

   아주 쉽게 이해가 될 것이다.

   이번에는 아주 재미있는 루틴이다. 인라인 어셈블리를 어떻게 C에서 define 해서 사용할 수 있는지의

   하나의 예로서 흥미롭게 살펴보자.

---------------------------------------------------------------------------------------
/*
* This looks horribly ugly, but the compiler can optimize it totally, as the count is constant.
*/
extern inline void * __constant_memcpy(void * to, const void * from, size_t n)
{
  switch (n) {
         case 0:
                return to;
         case 1:
                *(unsigned char *)to = *(const unsigned char *)from;
                return to;
         case 2:
                *(unsigned short *)to = *(const unsigned short *)from;
                return to;
         case 3:
                *(unsigned short *)to = *(const unsigned short *)from;
                *(2+(unsigned char *)to) = *(2+(const unsigned char *)from);
                return to;
         case 4:
                *(unsigned long *)to = *(const unsigned long *)from;
                return to;
  }

#define COMMON(x) \
__asm__("cld\n\t" \
  "rep ; movsl" \
  x \
  : /* no outputs */ \
  : "c" (n/4),"D" ((long) to),"S" ((long) from) \
  : "cx","di","si","memory");

  switch (n % 4) {
    case 0: COMMON(""); return to;
    case 1: COMMON("\n\tmovsb"); return to;
    case 2: COMMON("\n\tmovsw"); return to;
    case 3: COMMON("\n\tmovsw\n\tmovsb"); return to;
  }
#undef COMMON
}
---------------------------------------------------------------------------------------

리누스 토발즈 자신도 이번 루틴은 못 생겼다고 말하고 있는 듯하다.
C 의 case 문은 n이 0일 때는 그냥 리턴하고, 1,2,3일때는 unsigned char *, unsigned short *,로

void *를 형변환하여 포인터를 사용하여 값을 그대로 메모리로 적고 있다.

void *의 유용한 점은 어떠한 포인터 형으로도 변환될 수 있다는 데 있는 데, 이것을 잘 활용하고 있는 셈이다.

from과 to를 *(unsigned char *)로 값을 적는 다면 해당하는 곳에 1바이트를 적게 되고 *(unsigned short *)

로 취하면 2바이트를 적게 된다. 당연히 unsigned long을 취하면 4바이트를 적게 된다. 이 경우의 포인터에

1,2,3 등의 연산은 그 어떠한 포인터로 해당하는 주소가 참조되었느냐에 따라서, 증가하는 폭이 달라진다.

const는 from이 가르키고 있는 곳의 데이타를 상수형태로 취급함으로써 변경을 금지함을 컴파일러에게

알리고 있다. 마찬가지로 8,12...20과 같이 적은 수에는 그냥 바로 포인터 연산으로 값을 복사한다.

COMMON은 여기에서 특정부분에만 정의된다. 인라인 어셈에서 공통되는 부분을 따로 분리하여정의한

것이다. 내부의 인라인 어셈을 보면.. ecx에 n/4를, edi에 to를, esi에 from을 전달하고 있다. cld; rep; movsl;

로 인하여 n/4만큼 from에서 to로 4바이트 단위로 복사를 한다. 나머지 남은 여분의 1-3바이트는 define 을

적절히 활용한다.
COMMAN("\n\tmovsb")는 위의 define에 의해
__asm__("cld\n\t" \
        "rep; movsl" \
        "\n\tmovsb" \
        : ..
        .....
        );
로 해석이 된다. 나머지의 경우도 n%4바이트 만큼 복사를 하기 위하여 define을 활용한다.


2.16  memmove

memmove는 src에서dest로 n바이트만큼을 메모리 이동을 시키는 것이다.
memcpy와 거의 비슷하다고 볼 수 있으나 src와 dest가 겹치는 경우를 대비하여 정상적인 값을

복사한다는 점에서 조금 차이가 있다.

--------------------------------------------------------------------
extern inline void * memmove(void * dest,const void * src, size_t n)
{
if (dest<src)
__asm__ __volatile__(
  "cld\n\t"
  "rep\n\t"
  "movsb"
  : /* no output */
  :"c" (n),"S" (src),"D" (dest)
  :"cx","si","di");
else
__asm__ __volatile__(
  "std\n\t"
"rep\n\t"
  "movsb\n\t"
  "cld"
  : /* no output */
  :"c" (n),
   "S" (n-1+(const char *)src),
   "D" (n-1+(char *)dest)
  :"cx","si","di","memory");
return dest;
}
---------------------------------------------------------------------

목적지 주소 < 원천지 주소 일 경우와,
목적지 주소 >= 원천지 주소 일 경우로 나누어 처리하고 있다.
일반적으로 두개의 영역이 겹치지 않는다면 별 문제 없이 처리되지만 겹치는 경우를 예로 들어보자. 

다음과 같이 그림을 그려보면,

(1) dest < src

<------ low address          high address ------------>

    dest 영역     -------------------------+
                                           |
  +-------------------------------------------------------------+
  |   dest  영역            | 겹치는 영역  |     src 영역       |
  +-------------------------------------------------------------+
   abcdefg....              |12345.....
                            +--------------------- src 영역

이 경우에는 src의 12345부터 차례대로 dest의 abcdef...로 복사를 해주면 별 이상없다.
ecx에는 n, esi에는 src, edi에는 dest가 전달되어 cld; rep; movsb;로 정방향으로 src에서 dest로

n만큼 바이트 복사를 하게 된다.

(2) src <= dest

<------ low address             high address ------------>

    src 영역     -------------------------+
                                          | 
  +-------------------------------------------------------------+
  |   src  영역            | 겹치는 영역  |     dest 영역       |
  +-------------------------------------------------------------+
   12345...                |abcde..  ..789                ...xyz
                           +--------------------- dest 영역

이 경우에는 12345...(src)에서 abcde..(dest)로 바로 복사를 해버린다면 복사를 채 다하기도 전에 src의

뒷부분의 값이 변경되어 버려서 정상적인 복사가 이루어 지지 않을 것이다. 이때에는 src의 뒷부분인

9,8,7 부터 dest의 뒷부분인 z,y,x로 복사를 거꾸로 하면 정상적인 복사를 할 수 있다.
ecx에 n, esi에 (n-1+(const char *)src)를, edi에 (n-1+(char *)dest)를 각각 전달하고 있다.

즉, src, dest에서 n만큼을 더한 주소를 가르킨다. std; rep; movsb로 이번에는 거꾸로 src의 뒷부분에서

dest의 뒷부분으로 바이트 메모리 복사가 n번 이루어진다.


2.17  memchr

memchr은 strchr의 메모리 버젼이다.

-----------------------------------------------------------------
extern inline void * memchr(const void * cs,int c,size_t count)
{
register void * __res;
if (!count)
  return NULL;
__asm__ __volatile__(
  "cld\n\t"
  "repne\n\t"
  "scasb\n\t"
  "je 1f\n\t"
  "movl $1,%0\n"
  "1:\tdecl %0"
  :"=D" (__res):"a" (c),"D" (cs),"c" (count)
  :"cx");
return __res;
}
-----------------------------------------------------------------

cs가 가르키는 메모리에서 시작하여 c의 값이 있는지를 count번 검사를 한다. output은 edi로 통해서

__res로 결과값이 전달되고,input은 eax에 찾고자하는 값 c를, edi에 cs를, ecx에 count를 각각 전달하고

있다. 먼저 C에서 void * __res를 선언을 하고 count가 0이라면 NULL을 리턴한다. 아니라면 어셈블리 루틴을

실행한다. eax에는 int a의 값이 들어가 있으나 사실상은 eax의 하위 1바이트인 al만이 검사를 위해서

사용된다. cld; repne; scasb; 는 ecx(count)번 al(a의 하위1바이트)의 값과 edi(cs)값을 비교하여

같지 않다면 계속 반복하여 같은 값이 나오면 그 다음을 edi가 가르킨다. 따라서 발견한다면 je 1f로

decl %%edi를 하여 하나를 감소시킨 다음의 메모리 위치를 __res로 넘겨준다. 같은 값을 찾지 못했다면

movl $1, %%edi를 하여 edi에 1을 저장하고 decl %%edi로 0이 되어 __res는 NULL포인터를 리턴한다.


2. 18 memset

--------------------------------------------------------------
extern inline void * __memset_generic(void * s, char c, \
                         size_t count)
{
__asm__ __volatile__(
  "cld\n\t"
  "rep\n\t"
  "stosb"
  : /* no output */
  :"a" (c),"D" (s),"c" (count)
  :"cx","di","memory");
return s;
}
---------------------------------------------------------------

al(eax)에 c를, edi에 s를, ecx에 count를 전달하고 있다. cld; rep; stasb; 로 al(c)의 값을

edi(s)가 가르키는 곳에 ecx(count)번 반복하여 쓴다.

-----------------------------------------------------------------------
/*
* memset(x,0,y) is a reasonably common thing to do, so we want to fill
* things 32 bits at a time even when we don't know the size of the
* area at compile-time..
*/

extern inline void * __constant_c_memset(void * s, unsigned long c, \
                                       size_t count)
{
__asm__ __volatile__(
  "cld\n\t"
  "rep ; stosl\n\t"
  "testb $2,%b1\n\t"
  "je 1f\n\t"
  "stosw\n"
  "1:\ttestb $1,%b1\n\t"
  "je 2f\n\t"
  "stosb\n"
  "2:"
  : /* no output */
  :"a" (c), "q" (count), "c" (count/4), "D" ((long) s)
  :"cx","di","memory");
return (s); 
}
-----------------------------------------------------------------------

eax에 unsigned long c를, eax, ebx, ecx, edx 중 하나에 count를, ecx에 count/4를, edi에 s를 전달하고

있다. 값을 전달할 때 전달되는 인자형의 크기에 따라서 commands구문에서 적절히 사용하여야 한다.

그렇지 않다면 버그가 될 수 있을 것이다. cld; rep; stosl;로 eax의 값을 edi(s)가 가르키는 곳에 4바이트씩

ecx(count/4)만큼을 반복하여 쓴다. 이전에 본바와 마찬가지로 testb $2, %b1; testb $1, %b1; 과 같은 것으로

count의 하위 2,1번째 비트를 검사하여 각각 2바이트, 1바이트를 ax, al에서 가져와서 쓰고 있다.



2.19 strnlen

----------------------------------------------------------------------
/* Added by Gertjan van Wingerde to make minix and sysv module work */

extern inline size_t strnlen(const char * s, size_t count)
{
register int __res;
__asm__ __volatile__(
  "movl %1,%0\n\t"                 /* ecx -> eax */
  "jmp 2f\n"            
  "1:\tcmpb $0,(%0)\n\t"        /* cmpb $0, [eax] */
  "je 3f\n\t"                  
  "incl %0\n"                           /* eax++ */
  "2:\tdecl %2\n\t"                /* edx-- */
  "cmpl $-1,%2\n\t"                /* cmpl $-1, edx */
  "jne 1b\n"                   
  "3:\tsubl %1,%0"                    /* eax -= ecx */
  :"=a" (__res):"c" (s),"d" (count));
return __res;
}
/* end of additional stuff */
----------------------------------------------------------------------

output은 eax를 통해서 __res에 전달되고 있다. ecx에는 s를, edx에는 count를 input으로 전달하고 있다.

strnlen("abc", 3); strnlen("abc", 5); strnlen("abc", 2); 로 하여 각각 호출하면 3, 3, 2를 리턴한다.

즉, count안에서 s에서 널이 나오면 그 길이를 돌려주고 나오지 않으면 count 만큼을 리턴하는 것이다.

cmpb $0, (%0)은 s의 복사본 포인터를 하나 증가 시키고, incl %0은 s중 널이 아니라면 다음 곳을 가르키게

하며 decl %2는 count의 복사본을 하나 감소시키면서 s중 널문자를 만나거나 cmpl $=1, %2; 처럼 count의

복사본이 -1이 되면 진행된 s의 복사본에서 원래 s의 값을 빼서 돌려준다. 이것은 s의 문자열 길이이거나

count이다.


2.20 etc functions

나머지 몇개를 살펴보자.

이것도 이전의 memset과 거의 같다. 그러면서도 조금씩 다르게 작동하는 함수가 여러개이다.

-----------------------------------------------------------------------
/*
* This looks horribly ugly, but the compiler can optimize it totally,
* as we by now know that both pattern and count is constant..
*/
extern inline void * __constant_c_and_count_memset(void * s, unsigned long pattern, size_t count)
{
  switch (count) {
    case 0:
      return s;
    case 1:
      *(unsigned char *)s = pattern;
      return s;
    case 2:
      *(unsigned short *)s = pattern;
      return s;
    case 3:
      *(unsigned short *)s = pattern;
      *(2+(unsigned char *)s) = pattern;
      return s;
    case 4:
      *(unsigned long *)s = pattern;
      return s;
  }
#define COMMON(x) \
__asm__("cld\n\t" \
  "rep ; stosl" \
x \
  : /* no outputs */ \
  : "a" (pattern),"c" (count/4),"D" ((long) s) \
  : "cx","di","memory")

  switch (count % 4) {
    case 0: COMMON(""); return s;
    case 1: COMMON("\n\tstosb"); return s;
    case 2: COMMON("\n\tstosw"); return s;
    case 3: COMMON("\n\tstosw\n\tstosb"); return s;
  }
#undef COMMON
}
-----------------------------------------------------------------------
eax에 pattern을 넣고, ecx에 count/4를 넣고, edi에 s를 넣는다.
그다음 count가 적을 경우에는 그냥 바로 s에 접근하여 count 의 크기에 따라 patterns를 적고,

조금 클 경우에는 define 정의를 사용하여 어셈블리 루틴으로 처리한다.

cld; rep; stosl; 로 ecx에 저장된 횟수만큼 eax의 값을 edi(s)가 가르키는 곳에 4바이트씩을 쓴다.

그리고 count%4의 값에 따라 여분의 바이트를 쓰게 된다. 위에서 본바와 같다.


이번에는 memscan이다. 위에서 본 memchr과 거의 비슷하다.
---------------------------------------------------------------------
/*
* find the first occurrence of byte 'c', or 1 past the area if none
*/

extern inline void * memscan(void * addr, int c, size_t size)
{
  if (!size)
    return addr;
  __asm__("cld
    repnz; scasb
    jnz 1f
    dec %%edi
1:    "
    : "=D" (addr), "=c" (size)
    : "0" (addr), "1" (size), "a" (c));
  return addr;
}
-----------------------------------------------------------------------

input은 edi에 addr을 , ecx에 size를 , eax에 찾을 값인 c를 전달한다.
output은 edi를 통하여 addr로 값을 전달하고, ecx를 통하여 size에 값을 전달한다.

먼저 size가 0이면 addr을 그냥 리턴한다.
cld; repnz; scasb; 로 al(c의 하위1바이트)의 값과 edi(addr)이 가르키는 곳의 값이

같은 지를 바이트 단위로 ecx(size) 번 검사하여 한다.
찾았다면 dec %%edi로 edi를 하나 감소시켜서 찾은 위치를 addr로 전달하고 못찾았다면

현재 별 의미 없는 곳을 가르키고 있는 edi를 그대로 addr로 전달한다.

 

 


3. 덧붙이는 말

이제 분석할 string.h에 더이상 인라인 어셈블리가 없다. 혹시 의구심이 나는 부분이 있으면

그 부분만 따로 떼어 내어서 함수이름을 적절히 바꾸어서 테스트를 해 볼 수도 있다.

아울러 작성한 인라인 어셈블리가 포함된 소스를 컴파일 할 적에 gcc -S my_memcpy.c 와 같이 하여

순수 어셈블리 루틴만을 구할 수 도 있다.

조금은 예외지만 설명하지 않은 것 중에 define 된 것을 살펴보자.

-----------------------------------------------------------------------
/* 1 */
#define memcpy(t, f, n) \
(__builtin_constant_p(n) ? \
__constant_memcpy((t),(f),(n)) : \
__memcpy((t),(f),(n)))

#define memcmp __builtin_memcmp

/* 2 */
#define __constant_count_memset(s,c,count) \
                     __memset_generic((s),(c),(count))

/* 3 */
#define __constant_c_x_memset(s, c, count) \
(__builtin_constant_p(count) ? \
__constant_c_and_count_memset((s),(c),(count)) : \
__constant_c_memset((s),(c),(count)))

/* 4 */
#define __memset(s, c, count) \
(__builtin_constant_p(count) ? \
__constant_count_memset((s),(c),(count)) : \
__memset_generic((s),(c),(count)))

/* 5 */
#define memset(s, c, count) \
(__builtin_constant_p(c) ? \
__constant_c_x_memset((s),(0x01010101UL*(unsigned char)c),(count)) : \
__memset((s),(c),(count)))


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


몇가지 매크로를 정의하고 있는 데, 모두다 __builtin_constant_p()라는 것에 의존하고 있다.

커널소스나 헤더파일에는 아무리 찾아봐도 이런 정의나 선언이 존재하지 않는다.

또 이것은 다른 많은 커널 소스속에서도 나타나고 있다.

컴파일러 소스나, libc속에 있을 법도 한데 아뭏던 첫부분의 메크로 정의(1)에서는 memcpy를 호출하면

__builtin_constant_p(n)이 0이면 __memcpy (뒤에 나올 매크로)가 호출되고 아니면 __constant_memcpy가

호출 된다. 전자는 4바이트단위기본복사와 1-3의 나머지 복사를 하는 순수 인라인 어셈블리루틴이고,

후자는 앞전에 보았던 switch 문과 인라인 어셈을 혼용한 루틴이다.

(3)를 보면,
__constant_c_x_memset을 호출하면 __builtin_constant_p(count)의 값에 따라서 0이면

__constant_c_memset(4바이트단위복사 인라인어셈루틴)이 사용되고 아니면

__constant_c_and_count_memset(switch와 인라인을 혼용한 루틴)이 사용된다.

(4)을 보면,
__memset을 호출되면 __builtin_constant_p(count)의 값에 따라서 0이면
__memset_generic이 사용되고, 아니면 __constant_count_memset이 사용된다.
(2)의 매크로 정의로 인해 __memset_generic은 __constant_count_memset에 대한 매크로로 같다.

(5)를 보면,
memset을 호출하면 __builtin_constant_p(c)의 값에 따라서 0이면 __memset이 불리워지고 아니면 __constant_c_x_memset((s), (0x01010101UL*(unsigned char)c), (count)); 로 불리워진다.

후자는 또다시 (3) 에 나오는 매크로이다.

매크로를 제외한 memset과 관련된 함수만 정리하자.

__memset_generic                      : 1바이트씩 복사 인라인 어셈
__constant_c_memset                 : 4바이트씩 복사 인라인 어셈
__constant_c_and_count_memset : switch와 4바이트 복사 어셈 혼용

 


4. 나오는 말

여기까지 다 보신분에게 박수를 보내드리고 싶다. 이제 AT&T문법의 인라인 어셈에는 어느정도 아실 것이며

여러분들의 프로그램에 필요한 만큼 인라인 어셈을사용하실 수도 있을 것이다. 커널 소스를 본격적으로

분석하자면 AT&T 문법에 기반한 어셈블리는 필수적으로 알아야 한다. 그렇지 않다 하더라도 빠른 속도처리를

요하는 곳에는 한번 쯤 사용해 볼만하다. 휴..

마지막으로 메모리 접근과 관련된 벤치마크 프로그램을 아래에 싣겠다.
1바이트,4바이트 단위로 복사를 하거나 memset을 하는 인라인 어셈블리 루틴들이다.

string.h 의 제일 첫부분에 토발즈의 언급이 있긴 하지만 과연 어느것이 더 빠를 것인가?

여러분의 시스템에서도 한번 실행해보기 바란다. 그럼, 다음 기회에 또만날 것을 약속하며....

또치 한동훈

ddoch@hitel.kol.co.kr
ddoch@nownuri.nowcom.co.kr

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

/* benchmark.c -- memory access speed benchmark test program    
*                between 1 byte copying and 4 byte copying, and
*                measuring 1/100 second.
*    
* My system is 486DX4-100, gcc 2.7.2. Follows is the result.
* I've been to compile 'gcc -O2 benchmark.c.
*
*                asm memset (byte): 198   (1/100 second)
*                asm memset (long): 48    (1/100 second)
*                libc memset      : 48    (1/100 second)
*               
*                asm memcpy (byte): 341   (1/100 second)
*                asm memcpy (long): 190   (1/100 second)
*                libc memcpy      : 190   (1/100 second)
*
*                by ddoch 1997.2.20 e-mail ddoch@hitel.kol.co.kr
*/

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

#define MEMORY_ALLOC_SIZE   (1024*1024)
#define TEST_NUMBERS        (10)

inline void * byte_memset(void * s, char c, size_t count)
{
     __asm__ __volatile__(
                                     "cld\n\t"
                                     "rep\n\t"
                                     "stosb"
                                     : /* no output */
                                     :"a" (c),"D" (s),"c" (count)
                                     :"cx","di","memory");
     return s;
}

inline void * long_memset(void * s, unsigned long c, size_t count)
{
     __asm__ __volatile__(
                                     "cld\n\t"
                                     "rep ; stosl\n\t"
                                     "testb $2,%b1\n\t"
                                     "je 1f\n\t"
                                     "stosw\n"
                                     "1:\ttestb $1,%b1\n\t"
                                     "je 2f\n\t"
                                     "stosb\n"
                                     "2:"
                                     : /* no output */
                                     :"a" (c), "q" (count), "c" (count/4), "D" ((long) s)
                                     :"cx","di","memory");
     return (s); 
}

inline void * byte_memcpy(void * to, const void * from, size_t n)
{
     __asm__ __volatile__(
                                     "cld\n\t"
                                     "rep; movsb\n\t"
                                     : /* no output */
                                     : "c" (n), "S" ((long)from), "D" ((long)to)
                                     : "cx", "si", "di");
     return (to);
}

inline void * long_memcpy(void * to, const void * from, size_t n)
{
     __asm__ __volatile__(
                                     "cld\n\t"
                                     "rep ; movsl\n\t"
                                     "testb $2,%b1\n\t"
                                     "je 1f\n\t"
                                     "movsw\n"
                                     "1:\ttestb $1,%b1\n\t"
                                     "je 2f\n\t"
                                     "movsb\n"
                                     "2:"
                                     : /* no output */
                                     :"c" (n/4), "q" (n),"D" ((long) to),"S" ((long) from)
                                     : "cx","di","si","memory");
     return (to);
}

void main()
{
       void *src, *dest;
       clock_t c1, c2;
       int i;

       /* memory allocation */
       src  = (void *) malloc(MEMORY_ALLOC_SIZE);
       if (src == (void *)0) 
       {
              fprintf(stderr, "Memory allocation error!!\n");
              fprintf(stderr, "Be decreased allocation memory size\n\n");
              exit(1);
       }

       dest = (void *) malloc(MEMORY_ALLOC_SIZE);
       if (dest == (void *)0) 
       {
              fprintf(stderr, "memory allocation error!!\n");
              fprintf(stderr, "Be decreased allocation memory size\n\n");
              exit(1);
       }

       /* memset test */
       printf("\nmemset testing...\n\n");
       /* memset byte assembly */
       c1 = clock();
       for (i = 0; i < TEST_NUMBERS; i++) 
       {
              byte_memset(src, 0, MEMORY_ALLOC_SIZE);
       }

       c2 = clock();
       printf("asm memset (byte): %d   (1/100 second)\n\n", c2-c1);

       /* memset long assembly */
       c1 = clock();
       for (i = 0; i < TEST_NUMBERS; i++) 
       {
              long_memset(src, 0, MEMORY_ALLOC_SIZE);
       }

       c2 = clock();
       printf("asm memset (long): %d   (1/100 second)\n\n", c2-c1);

       /* memset in libc */
       c1 = clock();
       for (i = 0; i < TEST_NUMBERS; i++) 
       {
              memset(src, 0, MEMORY_ALLOC_SIZE);
       }
     

       c2 = clock();
       printf("libc memset: %d   (1/100 second)\n\n", c2-c1);

       /* memcpy test */
       printf("\nmemcpy testing...\n\n");

       /* memcpy byte assembly */
       c1 = clock();
       for (i = 0; i < TEST_NUMBERS; i++) 
       {
              byte_memcpy(dest, src, MEMORY_ALLOC_SIZE);
       }
     

       c2 = clock();
       printf("asm memcpy (byte): %d   (1/100 second)\n\n", c2-c1);

       /* memcpy long assembly */
       c1 = clock();
       for (i = 0; i < TEST_NUMBERS; i++)
       {
              long_memcpy(dest, src, MEMORY_ALLOC_SIZE);
       }

       c2 = clock();
       printf("asm memcpy (long): %d   (1/100 second)\n\n", c2-c1);

       /* memcpy in libc */
       c1 = clock();
       for (i = 0; i < TEST_NUMBERS; i++)
       {
              memcpy(dest, src, MEMORY_ALLOC_SIZE);
       }
     

       c2 = clock();
       printf("libc memcpy: %d   (1/100 second)\n\n", c2-c1);

       free(src);
       free(dest);
}

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