다시 CSAPP 시간입니다. 추석에 실컷 놀고.. OS랑 통계 공부하다 다시 돌아왔습니다. 원래부터 사람은 당장 어떤 위험이 눈앞에 닥치기 전까진 실감을 잘 못하는 것 같습니다. 분명히 근 시일 내에 치나 3차 쓰알이 나올거라 알고 있었음에도, 성능에 한눈에 팔려 감사제 코토네에 반천장 돌을 박은 제가, 분명 분명 알고 있었는데 왜 그랬는지 이해가 가지 않습니다. 남은 돌은 긁어 모아도 반 천장이고, 출시는 다음주 입니다. 노래가 별로면 패스해도 되겠죠~~ 근데.. 왜 작곡에 에반콜이 보일까요...? 오케스트라 거장이 왜 우리 게임에 왔냐고.. 부자집 영애님이라는 설정 맞추려고 진짜 돈 다 털어서 모셔온거야?? 근데 이러면 노래가 별로일 수가 없는거 아냐?? 담주에 안보이면 시험 끝나자마자 돌 캐러 간겁니다...
무튼 이전 글에서는 조건 연산 인스트럭션까지 정리했습니다. 임의의 조건 연산 결과를 각 플래그 레지스터들을 통해 반영하여, 설계한 분기점으로 코드 흐름을 이끌어 낼 수 있습니다. 이번에는 이 '흐름'을 직접적으로 동작시키는 인스트럭션을 살펴보겠습니다. JUMP 클래스의 인스트럭션이 이러한 분기점 발생 시의 PC가 바라보는 주소값을 직접적으로 변경시킵니다.
JUMP 인스트럭션
이전에서 공부했던 것처럼, 일반적인 경우에는 인스트럭션들은 모두 나열된 순서대로 실행됩니다.(물론 아닌 경우도 있습니다. OStudy 카테고리에서 작성했던 멜트 다운 취약점에 대한 설명에서 다루었던, 비순차적 실행 등이 그 예시입니다.) 하지만, 우리가 분기점을 생성한다면, 결국 어느 한 분기는 반대편 분기의 코드를 건너뛰고 해당 코드부터 실행되도록 유도해야 할 것입니다. 하지만 이전의 조건 연산 인스트럭션에서는 직접적으로 PC의 값을 변경하는 연산은 존재하지 않았습니다. 이러한 연산은 JUMP 계열의 인스트럭션이 담당하기 때문입니다.
점프 인스트럭션은 프로그램이 완전히 새로운 위치로 실행되도록 전환시킵니다. 특히, 이 점프 인스트럭션은 오퍼랜드로 임의의 변수와 같은 라벨(Label)을 사용합니다. 점프로 이동할 지점을 라벨로 표시해두고, 조건 분기에 따라 알맞는 라벨로 이동하는 방식입니다. 보통의 오퍼랜드도 받습니다. 위와 같은 방법을 직접 점프(Direct jump), 그리고 오퍼랜드를 읽어와 점프하는 방법을 간접점프(Indirect jump)라고 합니다.
점프 또한 기초적인 jmp 인스트럭션에서 SET 클래스 등과 같은 접미사를 공유합니다. 아래의 표를 보면 한눈에 보이겠지만, 이전 포스팅의 조건 연산 인스트럭션은 분기 플래그를 '설정'하는 담당이었다면, 이러한 점프 인스트럭션에서는 설정된 분기 플래그를 '사용'하는 담당입니다. 조건부 점프는 모두 목적지가 라벨입니다. 즉 조건부 점프는 직접 점프만 가능합니다.

점프 인스트럭션 인코딩
그렇다면, 우리가 따로 라벨이라는걸 지정하는 것도 아닌데 어떻게 라벨이 자동으로 만들어지고 점프가 알아서 되는걸까요? 이후 링커에 대해 공부할 때 보다 자세하게 알아보겠지만, 지금 어느정도 설명하고 가는게 향후 이해에 도움을 줄 수도 있겠습니다. 언급한대로 점프 인스트럭션은 라벨이라는 별도의 변수를 사용합니다. 이는 어셈블리 코드에서도, 별도의 주소값 자체를 받지 않음을 뜻합니다. 정확히 메모리의 어디로 점프해야하는지(이조차 절대적인 주소가 아니지만)는 링커를 통해 인코딩됩니다. 보편적인 방법이 PC 상대적 방법(PC relative)인데, 해당 인스트럭션과, 점프 이후의 인스트럭션 주소와의 차이 즉 오프셋을 인코딩하는겁니다. 절대적인 주소를 사용하지 않습니다. 이는, 만약 다른 주소에서 코드를 실행되는 경우에 대해 절대적 주소 방법은 이 상황에 대처할 수 없기 때문입니다.
지금 이 명령어가 어디서 실행되든, 다음 명령어는 어차피 동일하게 n 오프셋 뒤에 위치해 있습니다. 그렇기에 절대적인 주소를 담지 않고 상대적인 거리로 잡아 PC 값을 변경시킵니다. 물론 절대 주소 방법도 사용하는 경우 역시 존재합니다. 이 경우는 대상 주소를 직접적으로 4바이트로 저장하고, 그냥 PC를 해당 값으로 덮어주면 됩니다. 실제로 어셈블러가 생성한 오브젝트 파일을 열어보면, jump 인스트럭션 계열 옆에 상수값들이 붙어, 점프 범위를 설정해주는걸 확인할 수 있습니다.(이 경우, 명령어 길이 + 해당 상수값으로 점프 범위를 계산합니다.) 아래의 어셈블리 코드를 예로 들어보겠습니다. jump 명령어인 eb 옆에 03이 붙습니다. 우리는 시작점으로부터 8만큼 떨어진 test 명령어까지 점프해야합니다. 하지만 3만큼 점프하라고 적혀있습니다. 이는, jump 명령어의 시작점이 3인데, 명령어의 길이는 2로 잡혀있으니, 8 - (3 + 2) = 3이므로 jmp의 거리를 3으로 잡은 것입니다.

CMOVE 인스트럭션
조건부 복사 인스트럭션 또한 존재합니다. CMOVE 계열의 인스트럭션은 명칭 그대로, 조건에 만족하면 목적지로 소스 값을 복사하는 인스트럭션입니다. 당연하게도, 소스와 목적지 두개의 오퍼랜드가 요구됩니다. 소스는 메모리나 소스 레지스터로부터 읽히나, 목적지에는 조건 만족 시에만 복사됩니다. 또한 단일 바이트의 이동은 불가능하며, 16, 32, 64비트 길이만을 갖습니다. 조건부 MOVE 인스트럭션 역시 기존의 다른 조건 인스트럭션과 마찬가지의 접미사가 붙으며 동일한 동작을 보입니다.
아래의 그림을 보면 알 수 있지만, 무조건적인 길이 명시가 이루어져있지 않습니다. 이는 CMOVE 인스트럭션의 경우, 목적지 레지스터의 이름으로부터 소스의 크기를 추정하는 방식이기 때문입니다. 따라서 모든 오퍼랜드 길이에 대해 동일한 인스트럭션을 사용할 수 있습니다.

조건부 이동 방식
OStudy에서 언급한 방식이 드디어 등장합니다. 위에서 언급한 가장 기본 방식인, 조건부 전환을 볼까요. 조건이 만족할 경우 프로그램의 한 가지 실행 경로를 따르게 하고, 아니면 다른 실행 방식을 따르게 하는 로직은, 해당 조건이 연산될때까지 프로그램은 멈춰있을겁니다. 그리고 이 조건문이 겹겹으로 쌓여 있다면 어떨까요? 충분히 쌓인다면 귀향길 국도 교통체중과 구분하기 어려울지도 모릅니다. 특히 오늘날의 CPU는 파이프라인 방식을 채용한 상태입니다. 하나 완료하고 다음 하나를 받는 게 아니라, 하나 찍는 동안 옆에서 하나 넘기고 바로 찍고 넘기고.. 명령어 진행에 대한 오버헤드를 극한으로 낮추는 방식입니다. 이는, 분기점 구현에서도 필요한 내용이었습니다. 위와 같은 정석적인 방법으로는, 파이프라인의 장점을 잡아먹기만 할 뿐입니다. 그렇기에 등장한 기법이 조건부 전송 기법입니다. 조건부 동작의 산출값들을 모두 계산하고 분기점을 구분하는 연산이 완료될 때까지 대기합니다.(정확히는 또다른 명령어를 받아서 실행하겠지만.. 적어도 이 함수만을 생각하면 대기하는 것과 같겠죠.) 그리고 분기점이 설정되면, 해당 분기점에 맞는 결과만 뱉어주면 됩니다! 물론 그만한 프로세서 성능특성이 뒷따라줘야 하지만, 현대의 프로세서라면 문제 없습니다. 이는 파이프라인의 형태가, 각각 요구된 동작의 작은 부분만 실행하고 다음 단계로 넘긴다는 실행 설계를 지녔기에 극대화시킬 수 있는 효율 기법입니다. 분기 연산을 한 실행 파트에서 계산하는 동안, 다른 쪽에서는 미리 실행 결과를 연산하는 방식의 병렬성을 얻을 수 있기 때문입니다. 여기서 더 나아가서 이야기해볼까요, 프로세서는 각 점프 인스트럭션이 실제로 실행될지를 추측하기 위한, 복잡한 분기예측 회로가 채택되어 있습니다. 이 경우 예측된 연산을 우선 시행하면 됩니다. 예측에 성공할 경우 랜덤의 경우에 비해 대략 9사이클 가까이 이득을 보게 되고, 예측 실패가 발생할 경우 랜덤 대비 약 3사이클 대의 손실을 보입니다.(랜덤일 시 17.5 사이클의 평균 소모를 보였을 때 기준)
물론 이러한 최적화 기법을 통해 생성되는 신박한 취약점도 존재합니다.(보안 계열에선 웃으며 못보겠지만요..) 위의 비순차적 실행 방식을 이용한 커널 메모리 탈취 취약점인 멜트다운 취약점에 대해선 해당 글에 정리되어 있습니다.
https://zeka0228.tistory.com/25
[OStudy] 5주차 - 메모리 가상화(1)
이제 본격적으로 메모리 쪽을 살펴볼 차례입니다. 가상화 라는 개념은 개인적으로 굉장히 매력적인 기법이라고 생각합니다. 이 가상화는 현대 시대에 들어, 어마어마한 기술적 진보가 이룩할
zeka0228.tistory.com
위와 같은 방식으로 인해 CMOVE 인스트럭션을 실행할 수 없는 경우도 발생합니다. 테스트 결과에 상관 없이 참/거짓일 때 연산이 모두 이루어진다는 것은(예를 들어 0 주소값 접근 또는 NULL 포인터 역참조 등등..), 만약 둘 중 하나에 문제가 발생했을 때 분기점의 참 거짓과 상관 없이 프로그램이 뻗을 수 있다는 의미가 됩니다. 또한 둘 중 한쪽이 매우 막대한 연산을 차지할 경우 추가적인 비효율을 야기할 수 있습니다.
'CS' 카테고리의 다른 글
| [CSAPP] 3챕터 - 프로그램의 기계수준 표현(7) - 조건 연산 인스트럭션 (1) | 2025.09.24 |
|---|---|
| [CSAPP] 3챕터 - 프로그램의 기계수준 표현(6) - 산술 연산 인스트럭션 (0) | 2025.09.10 |
| [CSAPP] 3챕터 - 프로그램의 기계수준 표현(5) - 데이터 이동 인스트럭션 (1) | 2025.09.09 |
| [CSAPP] 3챕터 - 프로그램의 기계수준 표현(4) - 오퍼랜드 (0) | 2025.09.06 |
| [CSAPP] 3챕터 - 프로그램의 기계수준 표현(3) - 스택 프레임 규칙 (0) | 2025.09.02 |