CS

[CSAPP] 3챕터 - 프로그램의 기계수준 표현(7) - 조건 연산 인스트럭션

Zeka_P 2025. 9. 24. 19:23

 이제 좀 패턴이 선 것 같습니다. 이터널리턴도 메테오라이트를 찍었고(릴까지 가기엔, 점점 롤할 때 인성으로 돌아가는 기분이라 못하겠더라고요.) 모비노기도 길드 함선 건조 끝났고.. 가쿠도 인자작 적당히 했습니다. 난 치나 발사대가 될거야!!! 남은 건, 곧 올 아키라 솜인형이 DIY라 바느질 배워야 한다는거 정도? 아직 도안만 받아봤는데 정말 잘 뽑혔더라고요. 지인P랑 같이 맡긴거라 커미션비 반띵하니까 비용도 얼마 안나온.. 10월에 받을 수 있다 하니, 바로 뚝딱뚝딱 해서 11월 라이브에는 이 친구들로 데려갈 것 같습니다. 아 이러면 가방도 하나 마련해야하는데.... 하 콜라보 굿즈로 지갑 말고 가방을 샀어야 했던건데... 완성하면 자랑하러 오겠습니다. 진짜 너무너무너무너무 귀여움 저거

 

그럼 이제 다시 할거 해야죠. 이번 포스팅에선 이전의 산술 연산 인스트럭션에 이어서, 제어문 관련 인스트럭션을 설명하겠습니다. 이전의 산술 인스트럭션들은, 단순한 동작을 의미했습니다. PC는 그냥 일직선으로 하나하나 코드를 타고 내려가며 선형적인 실행 흐름을 보였습니다. 하지만 실제 대다수의 프로그램은 단순한 선형 흐름을 보이지 않습니다. 대표적으로는 IF문이 있죠, else에 걸려버리는 순간, 조건이 참일 때의 코드 줄은 스킵하고 바로 else를 실행하는 거니까요. 이럴 때, 어셈블리 코드는 어떤 구성을 보여줄까요? 그냥 확 PC를 지정된 위치로 이동만 시키면 될까요? 참인지 거짓인지 판단은 어떻게 할까요? 조건 결과에 대한 값은 유지할까요? 이러한 내용들에 대해 다뤄볼 겁니다.

 

조건 코드 개요

 인스트럭션에 대해 설명하기 전에, 제어 계열 인스트럭션 사용에서 기초되는 조건 코드에 대해 먼저 언급하고 가겠습니다. 이전에 CPU에는 여러 레지스터가 존재하고, 특수 목적용으로 사용되는 별도로 구분된 독립 레지스터들이 존재한다 했습니다. 이들 중 일부가, 이번 제어 계열 인스트럭션에서 이용됩니다. 가장 최근의 산술이나 논리연산의 특성을 설명하는 특수 목적 레지스터들이 존재하며, 가장 중요한 것은 이들의 역할은 가장 최근 연산의 "특성"을 설명한다는 것입니다. 방법이 여러가지가 있었겠지만.. 가장 비용을 절약하는 방법은..? 그냥 비트 하나 껐다키는거로 하면 되는거겠죠. 대신 각 역할을 담당하는 레지스터들을 여러개 만들어 배치했습니다. 포켓몬에서 화상,독,잠듦,혼란,얼음을 담당하는 비트들이 따로따로 있는 것처럼요.(물론 살짝 비유가 다르긴 합니다. 포켓몬에서는 1바이트를 디버프용 바이트로 유지해서 8비트로 나눠 하나씩 껐다키는 식이지만.. 아무튼요) 이들을 흔히, 플래그(flag) 계열로 칭합니다. 종류는 아래와 같습니다.

 

CF (Carry Flag) : 캐리 플래그는 unsigned 연산에서 오버플로우가 발생했는지를 비트로 반영합니다. 

OF (Overflow Flag) : 오버플로우 플래그는 signed 연산에서 2의 보수 오버플로우가 발생했는지를 반영합니다.

ZF (Zero Flag) : 영 플래그는 가장 최근 연산 결과가 0이었는지를 비트로 반영합니다.

SF (Sign Flag) : 부호 플래그는 가장 최근 연산 결과가 음수였는지를 비트로 반영합니다.

 

딱봐도, ZF와 SF는 쉽습니다. 그냥 0이 나오면 ZF 올려 아니면 내려, -값이 나왔으면 SF 올려 아니면 내려 정도겠죠 뭐. 하지만 처음 보는 입장에선, CF와 OF는 조금 갸우뚱하게 됩니다. 오버플로우? 먼저 CF를 이야기하겠습니다.

CF는 unsigned 연산에서, 가장 상위 비트가 올림되거나(Carry) 버림(Bollow)될 경우 켜집니다. 쉽게 예를 들어보자면, 1111 1111이 있다 하면 현재 최상위 비트는 1입니다. 근데 여기다 0000 0001을 더하면 어떻게 될까요? 막연히 생각하면 0000 0001 0000 0000이긴 하지만, 현재 표현할 수 있는 범위를 초과합니다. 그러니 0000 0000이 되겠죠. 이를 두고, 오버플로우(Overflow)된다고 합니다. 이는 역으로도 마찬가지입니다. 0000 0000 에다가 0000 0001을 빼면? 1111 1111이죠. 그냥 단순 이진수로 보면 자연스러워보이지만, 우리가 사용하는 상수로 치환해보면 갑자기 엉뚱한 숫자가 나오는겁니다. 이게 큰 문제가 될까요? 당연히 됩니다. 문명 간디의 폭력성을 낮춰보셨나요? 간디의 폭력성이 0까지 도달한 상태에서, 한번 더 깎으면 어떻게 됐었죠? -1이 아니라 MAX에 도달합니다. 0000 0000 이었던게 -1 돼서 싹다 비트가 반전됐기 때문이죠.(1111 1111) 그렇습니다. 오버플로우 문제는 핵공격으로 인한 한 문명의 존망과도 직결될 수 있습니다! 위와 같은 이유로 CF를 통해 '최근 unsigned 연산에서 오버플로우가 발생했는가?"를 유지하는겁니다. 그럼 CF가 참일 경우 무언가 조치를 할 수 있겠죠. 순순히 CF를 통한 핸들링만 해준다면, 핵공격을 하지 않을 것입니다.

까지 썼는데 지난 20년에, 시드 마이어 측에서 공식적으로 해당 버그는 없었다고 공지하셨었네요. 뭐야 진짜 몰랐어. 

당신이 CF를 잘 살피지 않아, 오늘도 간디는 세계 최악의 독재자가 되었습니다. 역으로 아즈텍이 평화 문명으로 전환되는거도 가능할지도?

 

OF도 마찬가지입니다. 차이가 있다면 가장 왼쪽의 부호비트가 뒤집히냐가 오버플로우의 기준이 됩니다. 부호가 있는 정수는 예를 들어 1000 0100이 있다고 하면, 가장 최상단, 1000의 '1'이 부호를 의미합니다. 즉, 뒤의 7자리 000 0100이 수의 크기고, 따라서 구현 가능한 범위는 이 7비트 표현범위 이내에 한정해서만 가능합니다. 그렇다면 그 한계점, 예를 들어 0101 0000 에다가 0100 0000을 더해버리면 어떻게 될까요? 1001 0000? 단순히 덧셈이니 이렇게 나오겠지만, 이는 최상단 부호비트의 반전을 불러옵니다. 즉 수로 치환하면, 양수 + 양수를 했는데 음수가 나와버리는 대참사가 일어나버리게 되는겁니다! 

 

이 플래그들은, ADD나 SUB와 같은 연산 인스트럭션을 통해서도 변화하고, 아래 언급할 조건 인스트럭션을 통해서도 변합니다. 다만 INC나 DEC 인스트럭션에서는 오버플로우가 일어나도 OF나 ZF의 비트는 영향을 받을 수 있으나, CF에는 오버플로우 여부가 반영되지 않습니다. 또한 LEAQ 인스트럭션으로 인해서는, 주소 계산용 명령어 이기 때문에 플래그가 변하지 않습니다. 또한, 시프트 연산의 경우 시프트 되어 없어지는 마지막 비트로 CF에 반영됩니다.

아래는 비교 계열의 인스트럭션입니다. IF와 같은 분기점을 형성할 때 사용되는데, 우리가 IF문을 사용한다고 해서, 변수 자체의 값에는 영향을 주지 않듯, 이러한 조건 인스트럭션에서는 ADD나 SUB처럼 특정 오퍼랜드의 값이 변하는 인스트럭션이 아닌, 단순 비교용 인스트럭션이 별도로 필요합니다. 그렇기 때문에 그 결과의 특성을 저장하는, 위에서 언급한 특수 목적 레지스터들이 별도로 존재하는 것이고요.

 

먼저 CMP 인스트럭션은 두 오퍼랜드를 비교합니다. 예를들어 cmpq S1 S2 일 경우, S2-S1을 실행합니다. 만약 S2가 더 컸다면 CF 또는 OF는 꺼지고, 더 작았다면 CF나 OF가 켜졌을 것입니다. S2와 S1이 같았다면 결과는 0이 되니, ZF가 켜졌을 겁니다. 그렇다면 조건문에서는 CF나 OF, ZF만 조회해서 비교연산 결과를 확인하면 됩니다. 결과적으로 오퍼랜드 S1과 S2의 값은 변하지 않았음과 동시에, 비교 연산 결과를 조건 코드 레지스터에 담아, 분기점 형성이 가능하게 이끕니다. 

TEST인스트럭션은 AND인스트럭션과 동일한 역할을 하나, 위에서 언급한대로, 목적지 오퍼랜드에 영향을 주지 않고, ZF만 변경시킵니다.

 

조건 코드 활용

 이제는 이 조건 코드들을 어떻게 이용할지를 살펴보겠습니다. 이제는 우리가 IF문을 사용할 때와 동일한 사고로 가져가면 됩니다. 먼저 특정 조건에 맞춰 단일 바이트를 0이나 1로 설정시킵니다. 그리고 해당 비트값에 따라 특정 주소값으로 PC를 점프시킵니다. 그리고 데이터도 이동시킬 수 있겠죠. 이를 SET 인스트럭션을 통해, 특정 하위 레지스터로 단일 바이트를 설정시키는 방식으로 구현 가능합니다.

형성되는 접미사들은, e 인경우 equal'=', n은 not '~', ag는 각각 비부호, 부호 연산에서의 above, greater '>', bl은 각각 비부호, 부호 연산에서 below, less '<'를 의미합니다. 

 

가장 눈여겨 봐야 할 부분은 SF^OF입니다. 이 SF^OF, XOR 연산을 통해 논리성을 보장합니다. SF는 음수로 바뀌었을 때만 1로 켜지는 플래그입니다. 하지만 OF까지 켜졌다는 것은 오버플로우가 발생했다는 의미입니다. 그리고 오버플로우의 결과로 부호비트가 1이 되었다는 의미가 되니, 원래는 양수였다는 의미입니다. 따라서 양수 - 음수 와 같은 연산의 동작으로 양수가 음수로 오버플로우된 경우를 의미하고, 이 경우 생성된 음수는 진짜 음수가 아니니, less 연산에서는 이것이 가짜 음수를 거를 수 있도록 XOR 연산을 통해 결과 값이 음수여도(SF = 1) 이것이 오버플로우로 인해 발생된 값이라면(OF = 1) 이는 더 크다는 것으로 인지하겠다.(1 ^ 1 = 0)는 연산 의도를 담을 수 있게 해줍니다. 

비교 수식 a<b 를 예시로 어셈블리 코드를 형성할 시 아래와 같은 구현이 나옵니다. cmp로 비교하여 플래그를 최신화 시키고, setl을 통해 a가 b보다 더작은가를 SF^OF를 통해 구해 %al에 반영합니다. 이는 리턴을 담당하는 %eax로 비부호 확장의 형태로 복사되어 리턴됩니다.

comp:
    cmpq %rsi, %rdi
    setl %al
    movzbl %al, %eax
    ret

[CSAPP] 3ㅂ

 

챕터 - 프로그램의 기계수준 표현(6) - 산술 연산 인스트럭션ㅂ