반복(iteration)

이전 포스팅에서 조건과 무조건 jump를

설명할 때 high-level 언어인 C나 다른 언어의

문법인 분기문(if, switch case)이라던가

반복문(for, while)을 사용할 수 없다고 했는데

어셈블리어는 문법이 아니라 CPU에 직접

넣는 명령어라서 반복문도 직접 만들어야 합니다.

 

약간 뭐랄까 for문과 while 문을 원초적으로

재구성한다고 생각하면 됩니다.

물론 지금의 4차 첨단 사회 시대에는

프로그램을 개발할 때 고수준 언어에서

편리하게 제공하는 for 와 while를 그냥

사용하면 됩니다만, 그 루프의 원리를 따지고

보면 이러한 밑바닥 원리가 깔려 있다는 것이지요.

 

반복문이라고 하지 않고 그냥 '반복'을 말한 것은

이게 명령어 여러개를 포함하기 때문입니다.

카운터가 있어야 하고 카운터를 증감시킨 후

조건을 검사한 후에 점프하니까 여러개입니다.

 

원래 반복문은 분기문의 한 종류입니다.

분기문은 조건식의 결과에 따라 앞으로 가고

반복문은 특정 블록의 코드를 실행한 후에

다시 처음의 조건식으로 돌아오는 형태입니다.

 

고수준 언어에서 문(statement)의 개념은

어셈블리어 수준에서 보면 여러 명령어(instruction)의

조합으로 분해되는데 C언어의 if 분기와

while 루프는 매우 심오한 문법입니다.

 

 

현재의 선택에 따라 달라지는 미래에 대한 if 문과

과거의 코드로 돌아가서 반복하는 while 루프

철학적으로도 훌륭한 언어니까 지금까지도

널리 쓰이고 있다는 생각이 듭니다.

바꿔말하면 그게 프로그래밍의 사고방식을

규정하는 측면도 있겠지요. 어셈블리어는

쥐뿔도 없는 텅빈 공간에서 시작하지만

그런 사고의 얽매임은 없습니다.

 

그럼 기본 루프를 만들어 보겠습니다.

참고로 어셈블리어로 루프를 만드는 방법은

수도없이 많이 있습니다. rFlags(레지스터 플래그)로

점프를 할 수 있기 때문에 경우의 수가 많아집니다.

 

그런데 처음부터 이것저것 방법의 개수가

늘어나봤자 머리속에 혼란만 오니까

주력적인 방법부터 익숙해지는게 좋습니다.

 

for 나 while 문을 배웠을 테니까

생각을 해보는 겁니다. 루프를 하려면 뭐가 필요한가?

첫번째는 카운터 선언과 초기화입니다.

두번째는 비교연산이고 세번째는 카운터 증감,

마지막으로 반복할 코드의 블록이 있으면 됩니다. 

 

필요한 코드는 아래와 같습니다.

section .data    
    counter dq 5 ;초기 카운터

section .text
    xor rcx, rcx ;rcx 초기화 0
    mov rcx, qword [counter] ;rcx 를 루프 카운터로

_loop:
    push rcx ;rcx는 print에서 카운터로 사용
    print myLoop ;문자열 출력하는 매크로
    pop rcx ;루프의 counter를 복원

    dec rcx ;counter 감소
    cmp rcx, 0 ;0이 되는지 검사 
    jne _loop ;0이 안되면 ZF(zero flag가 1이다. -> jmp _loop)

카운터 rcx를 5에서 1씩 감소시켜서(dec)

0이되는 시점에 종료합니다.

jne (jump if not equal -> not jump if equal)

 

counter를 감소시키는 방식이 있고

또 증가시키는 방식도 있을 겁니다.

counter를 0에서 시작하고 cmp는

5를 비교하면 5번 출력합니다.

 

아래는 큰 수에서 작은 수로 내려오는 루프입니다.

 

%include "myInc.inc"

section .data
    text0 db "[-- Control-- ]", 10, 0

    myLoop db "- my Loop", 10, 0

    exitMSG db "*--- Program Closing...",10,0

    counter dq 5
    
section .bss
    ;no uninitialized data

section .text
    global _start

_start:
    nop ;just placeholder
    print text0

    xor rcx, rcx
    mov rcx, qword [counter]

    cmp rcx, 0
    jle _labelEnd ;counter less than or equal to 0

_loop:
    push rcx
    print myLoop
    pop rcx

    dec rcx ;counter
    cmp rcx, 0
    jne _loop
    nop
    exitProgram
    
_labelEnd:
    print exitMSG

_last:
    ;terminate the program
    exitProgram

 

myInc.inc 파일입니다.

%macro print 1
    mov rdi, %1
    call _strlen
    call _printString
%endmacro

%macro exitProgram 0
    mov rax, 60
    mov rdi, 0
    syscall
%endmacro

;**************************************
;separate _strlen and _printString
_strlen:
    push rdi ;push rdi
    xor rcx, rcx

_str_next:
    cmp [rdi], byte 0
    jz  _str_null

    inc rcx
    inc rdi
    jmp _str_next

_str_null:
    pop rdi ;pop rdi (rdi recovered)
    ret

_printString:
    mov rsi, rdi
    mov rdx, rcx
    mov rax, 1
    mov rdi, 1 ;stdout
    syscall
    ret

 

loop 명령어

loop 명령어는 복합명령어입니다.

아래 주석된 코드를 대체합니다.

자주 사용되니까 그냥 포맷으로 묶어버린건데,

중첩 루프를 사용하거나 할 때 rcx가

충돌할 수 있고 디버깅할 때 성가십니다.

간단한 경우 사용하면 좋겠지만

복잡한 루프문에는 잘 생각해볼 필요있습니다.

    ; dec rcx ;counter
    ; cmp rcx, 0
    ; jne _loop

    loop _loop

 

사실 C언어 컴파일러는 이런 루프 등 코드에

최적화가 잘되어 있습니다. 자세한 로직은 모르지만

웬만하면 수작업으로 짠 어셈블리어의 성능보다

좋다고 많이 검증이 되어 있습니다.

(처음부터 성능이 좋았던 C언어 이고 40년 넘게

컴파일러 최적화를 해왔으니 어떻게 보면 당연하다)

 

예제 코드 - 1부터 10까지 더하기

다음의 예제는 루프를 돌려서

1부터 10까지 더합니다.

 

extern printf

%macro printInt 1
    mov rsi, %1
    call _printfInt
%endmacro

%macro printStr 1
    mov rsi, %1
    call _printStr
%endmacro

section .data
    text0: db "[-- NASM basics --]",10,0

    intFormat: db "- value: (%d)",10,0
    strFormat: db "%s",0

    count: dq 0
    sum: dq 0

section .text
    global main

main:
    nop
    printStr text0
    

_loopStart:

    xor rcx, rcx
    mov rcx, qword [count]
    cmp rcx, 0
    jl _last

_loop:
    add qword [sum], rcx

    push rcx
    printInt [sum]
    pop rcx

    inc rcx ; decrease counter
    cmp rcx, 11
    jne _loop

    nop

_last:
    mov rax, 60
    mov rdi, 0
    syscall


_printfInt:
    push rbp
    mov rdi, intFormat
    ; mov rsi, rax
    mov rax, 0
    call printf
    pop rbp
    ret

_printStr:
    push rbp
    mov rdi, strFormat
    mov rax, 0
    call printf
    pop rbp
    ret

컴파일 옵션은 다음과 같습니다

(extern printf - 시스템에 따라 다를 수 있음)

all:
	nasm -f elf64 -o main.o -l main.lst main.asm
	gcc main.o -o main -no-pie
	./main

NASM 루프 합계

 

 

tutorials/Assembly/Nasm/iteration at main

 

GitHub - neokayken/tutorials: programming tutorial for beginner

programming tutorial for beginner. Contribute to neokayken/tutorials development by creating an account on GitHub.

github.com

 

공유하기

facebook twitter kakaoTalk kakaostory naver band