지난 글까지 산술 명령어, 논리 명령어, 비트 명령어를 정리하였다.
이번 글부터는 분기 명령어에 대해 정리하고자 한다.
Flow Control In Assembly
흐름제어는 한다면 코드가 위에서부터 아래로 한 줄씩 실행되는 것이 아니라, 특정 위치로 건너가 실행하도록 설정하는 것을 의미한다.
C언어에서 흐름 제어 키워드를 생각을 해보면 아래와 같은 키워드가 있다.
반복 : for, while, do while
분기 : switch, if, else
기타 : goto, 함수
그리고 강제로 이동하는 goto 문을 제외하면, 건너 뛸지 말지 여부를 결정하기 위한 '조건 체크'가 반드시 필요하다.
조건 체크는 비교 연산자를 사용한다. (!=, ==, <, >, ...등등)
어셈블리의 관점에서 흐름 제어도 마찬가지이다.
어셈블리는 명령어를 하나씩 하나씩 순차적으로 실행시키는 것이 기본이다.
(즉, 프로그램 카운터에 담긴 명령어 주소가 4byte 씩 증가한다.)
여기에 흐름 제어를 한다면, 명령어를 순차적으로 하나씩 실행시키다가, 다른 위치의 명령어로 건너가 실행할 것이다.
(즉, 프로그램 카운터에 담긴 명령어 주소가 완전 다른 값으로 세팅된다.)
어셈블리의 분기에는 3가지가 있다.
1. 무조건 분기 (Conditional branch)
2. 조건 분기 (Unconditional branch)
3. 함수 호출 (function call)
이 때, 분기 시에는 '조건을 판별' 하고 나서 분기가 이루어지기 때문에,
Data Hazard 에서 다뤘던 대로 분기 시 Delay가 발생한다.
때문에 분기 시에는 반드시 Branch Delay Slot 이 생긴다.
(한가지 궁금증은, 무조건 분기를 할 때도 Branch Delay Slot 이 왜 생길까? 조건 판별을 위한 시간이 필요가 없지 않나)
=> 생각해보니 조건식 계산은 분기 명령어 전에 있고, 그 계산 결과 (CC) 를 분기 명령어가 판별하는 건데, 판별하는데 2사이클이 필요하다고 한다면 ba도 '무조건' 이라는 걸 판별하는데 2사이클이 필요한 것이려나
분기를 할 지 말 지 결정할 때는, 현재 설정된 Condition Code 를 사용한다.
(왜 이름이 Condition Code 인지 이제 이해가 된다)
우리가 if 문에 사용하고, for 문에 사용하는 그 조건식들은 모두 대소비교나 같은지 다른지 판별하는 것들인데,
이 비교는 Condition Code 4가지 종류를 활용하면 모두 판별할 수 있다.
그래서 분기명령어를 실행하기 전에는 Condition Code 를 발생시키는 명령어를 사용해 Condition Code 를 발생시켜야 한다. 분기 명령어들은 모두 현재 설정된 Condition Code 를 보고 분기 여부를 결정한다.
분기 명령어의 종류
분기 명령어에는 아래와 같은 종류가 있다.
ba | Branch Always | (무조건 분기) | |
bn | Branch Never | (무조건 분기) | 얘는 왜 있는 걸까? 활용 방법이 궁금하다. |
be | Branch on Equal | A == B | |
bne | Branch on Not Equal | A != B | |
bl (blu) | Branch on Less | A < B | u 는 unsigned 일 때 |
ble (bleu) | Branch on Less or Equal | A <= B | u 는 unsigned 일 때 |
bg (bgu) | Branch on Greater | A > B | u 는 unsigned 일 때 |
bge (bgeu) | Branch on Greater or Equal | A >= B | u 는 unsigned 일 때 |
우선 무조건 분기는 Condition Code 의 체크없이 분기를 수행하기에 설명을 할 게 없다.
조건 분기의 설명에 앞서, 컴퓨터에서의 '비교' 는 '뺄셈' 과 같다.
두 수가 같은지 확인할 때는 두 수를 빼서 0인지 보면되고,
두 수 중 누가 큰 지를 볼 때는, 두 수를 빼서 부호가 양수인지 음수인지를 보면 된다.
그런데 Condtiion Code 를 보면 양수, 음수, 0의 판별을 Z, N, C, V 를 이용해 모두 할 수 있음을 산술 연산자 파트에서 정리하였다.
그때 정리했던 것을 다시 생각해보며 각 분기 명령어가 어떤 컨디션 코드를 체크하여 분기를 체크하는지 정리해보자.
be | A == B | A - B == 0 | Z |
bne | A != B | A - B != 0 | not Z |
bl | A < B | A - B < 0 | N xor V |
blu | C | ||
ble | A <= B | A - B <= 0 | (N xor V) or Z |
bleu | C or Z | ||
bg | A > B | A - B > 0 | not { (N xor V) or Z } |
bgu | not ( C or Z) | ||
bge | A >= B | A - B >= 0 | not ( N xor V ) |
bgeu | not C |
be, bne 는 어렵지 않다. 뺀 값이 0 인지만 판별하면 되기 때문이다.
중요한 점은 signed integer 비교에서 음수인지 판별할 때, N 과 V 값을 모두 활용한다는 점이다.
전에 Signed Integer 에서 뺄셈에 대해 정리할 때 아래와 같이 연산 결과의 부호를 알 수 있다고 하였다.
V | N | 연산 결과 | 실제 결과 |
0 | 0 | 양수 | 양수 |
0 | 1 | 음수 | 음수 |
1 | 0 | 양수 | 음수 |
1 | 1 | 음수 | 양수 |
오버플로우가 발생했을 때는, N 부호를 반대로 해석해야 했다.
이를 표로 정리했을 때, 실제 결과가 음수인 경우를 보면 V, N 의 값이 다를 때이며, 이는 xor 연산 시 결과와 같다.
따라서 N xor V 를 한 값이 1 이라면 음수가 됨을 알 수 있다.
이대로는 확 이해가 되지 않아서 A, B 의 부호를 나눠 A - B 의 상황을 정리해보았다.
A | B | A - B 연산 결과 | A - B 오버플로우 발생시 |
양수 | 양수 | 양수 / 음수 | 양수 / 음수 |
양수 | 음수 | 양수 | 음수 |
음수 | 양수 | 음수 | 양수 |
음수 | 음수 | 양수 / 음수 | 양수 / 음수 |
A, B 의 부호가 다를 때는 뺄셈을 했을 때, 기대되는 결과의 부호가 명확하므로, 그 부호가 나오지 않았을 때, 원래 결과를 알 수 있었다.
하지만 A , B 의 부호가 같을 때는 기대되는 결과가 2가지 인데, 원하는 부호가 나오지 않았음을 어떻게 알 수 있을까?
왜 A, B 의 부호가 같을 때도 V, N 만 가지고 실제 결과의 부호를 알 수 있는 것일까?
부호가 같은 수를 뺀다는 것은, 부호가 다른 수를 더하는 것과 같다.
32bit Signed Integer 에서 양수 범위와 음수 범위를 나타내보았다.
각 양수 범위의 양끝단과 음수 범위의 양 끝단을 서로 더해보면, 그 결과 값이 32bit Signed Integer 에서 표현 가능한 범위 안에서 나옴을 알 수 있다.
따라서 A, B 의 부호가 같을 때는 V 가 반드시 0 이므로 N 의 부호만을 가지고 결과의 부호를 알 수 있다.
다른 명령어로 CC 코드 하나하나를 체크하는 명령어도 있다.
Z 값 자체를 체크해서 활성화 되어있으면 분기 와 같은 느낌이다.
물론 이 명령어들은 잘 사용하지 않는다고 한다.
CC 코드가 4개 있고, 각 코드마다 활성화 여부를 체크하는 명령어가 2개씩 있어 총 8개의 명령어가 있다.
bneg | branch on negative | N = 1 |
bpos | branch on positive | N = 0 |
bz | branch on zero | Z = 1 |
bnz | branch on not zero | Z = 0 |
bvs | branch on overflow set | V = 1 |
bvc | branch on overflow clear | V = 0 |
bcs | branch on carry set | C = 1 |
bcc | branch on carry clear | C = 0 |
이 때,
bz 는 be 와 같은 기능을 수행한다.
bnz 는 bne 와 같은 기능을 수행한다.
bcs 는 blu 와 같은 기능을 수행한다.
bcc 는 bgeu 와 같은 기능을 수행한다.
분기 구문 형식
이제 분기 구문의 형식을 보자.
분기 구문은 아래와 같은 형식으로 작성한다.
cmp A, B ! cmp A, B = subcc A, B, %g0
b? <label> ! b? is branch instruction, see the condition code created from cmp
[delay slot]
label: ~~ ! if b? is true, goto label
먼저 분기 명령어에서 사용할 cc 코드를 발생시킨다.
보통 cmp 를 사용하는데, 이전 글에서 살펴봤던 tst 명령어를 사용하기도 한다.
아무튼 cc 코드가 생성되기만 하면 된다!
분기 명령어를 조건에 맞게 입력한다.
분기 여부를 판별하는데는 2사이클이 사용되니 delay slot이 하나 생성된다.
예시1 ( if - else 구문 )
다음과 같은 코드를 SPARC 로 바꿔보자.
int main() {
int a = 100;
int b = 10;
if (a == 100) {
a = a/ 4;
} else {
a++;
}
b = b + a;
}
나중에 b 에 해당하는 레지스터에 break point 를 걸어두고, 값을 확인해볼 것이다.
예상되는 코드는 a 가 100 이므로 if 문 내부 코드가 실행되면서 a = 25가 되고,
b = b + a 에서 10 + 25 = 35 가 b 의 값이 될 것이다.
위와 같이 코드를 작성하였다.
이제 이 코드를 gdb 를 이용해 디버깅해보자
예상한대로 35 값이 잘 나왔다.
이제 한번 a 의 값을 101 로 바꿔보자.
다시 보니 의도된 코드랑 조금 달라서 수정을 하였다
이제 이 코드에 대해서 디버깅을 해보자.
처음에 a 에는 101 이 있었고, a != 100 이므로 else 블록으로 넘어가서 a ++ 이 실행되어 102가 된다.
이 값에 b 값인 10 을 더하므로 112 가 나오는 것이 맞다.
예시 2 ( whlie 반복문 )
아래와 같은 코드를 SPARC 로 작성해보고자 한다.
int main() {
int a = 0;
int b = 0;
while (a < 10) {
b += a;
a++;
}
return;
}
1부터 9 까지 수를 모두 더하는 코드이다.
우리는 b 에 45 가 저장되어야 함을 알고 있다.
한번 어셈블리 코드를 작성한 후, 디버깅을 해보자.
위와 같이 코딩하였다.
한번 gdb 를 사용하여 결과를 확인해보자.
a 값이 들어있는 l1 레지스터에는 예상한대로 10 값이 들어있어 조건을 빠져나오게 되었다.
b 값이 들어있는 l2 레지스터에는 예상한대로 45 값이 들어있다.
while 문을 작성할 때는 위와 같은 방식이 아닌, do - while 처럼 조건 체크식을 뒤로 빼고, 일단 뒤로 점프시키는 방식을 사용할 수도 있다.
코드로 한번 보자.
이번엔 do while 과 유사하게 조건 체크를 뒤로 빼고, 처음엔 조건 체크하는 부분으로 이동시킨다.
조건에 해당하면 loop 내부 코드를 돌고, 코드가 실행되면서 자연스럽게 조건 체크 명령어로 다시 돌아오게 된다.
아까 작성한 방법과 다르게, 조건 체크시 우리가 의도한 코드의 조건과 동일한 방식으로 작성하면 된다는 장점이 있다.
실행결과도 이전에 실행했던 대로 잘 나온다.
지금까지 분기에 대한 내용을 정리하였다.
다음 글에서는 분기 명령어를 실행할 때 nop 가 발생하는 이유를 하드웨어 구조적으로 이해해보겠다.
참고
https://en.wikipedia.org/wiki/SPARC
'CS > 어셈블리' 카테고리의 다른 글
[SPARC] 19. Delay Slot Optimization (0) | 2023.10.17 |
---|---|
[SPARC] 18. Branch Delay Slot 의 발생 이유 (0) | 2023.10.17 |
[SPARC] 16. 비트 연산 명령어 (0) | 2023.10.15 |
[SPARC] 15. 논리 연산 명령어 (0) | 2023.10.14 |
[SPARC] 14. 곱셈과 나눗셈 그리고 서브루틴 (2) | 2023.10.11 |