CH03 프로그램의 기계 수준 표현
CH3.1-3.5 정리는 아래 참고
https://helloahram.tistory.com/55
CH03 프로그램의 기계 수준 표현 3.1-3.5
컴퓨터는 하위 동작들을 인코딩한 연속된 바이트인 기계어 코드 machine code 를 실행한다컴파일러는 프로그램 언어의 규칙, 대상 컴퓨터의 인스트럭션 집합, 운영 체제의 관례 등에 따라 기계어
helloahram.tistory.com
3.6 제어문
C 의 일부 구문인 반복문, 스위치문들은 데이터에 적용된 시험 결과에 따라 일련의 연산이 실행되는 조건부 실행이 요구된다
기계어 코드에서는 조건부 동작을 구현하기 위해 두 개의 기본적인 낮은 수준의 방법을 제공한다, 데이터 값들을 시험해서 이 시험결과에 따라 데이터 흐름이나 제어 흐름을 변경한다
보통, C 와 기계어 코드의 인스트럭션들은 모두 프로그램에 나타나는 순서대로 순차적으로 실행된다
기계어 인스트럭션들의 실행 순서는 점수 jump 인스트럭션으로 변경할 수 있다, 점프 인스트럭션은 때에 따라서는 어떤 시험의 결과에 따라 프로그램의 다른일부분으로 제어를 넘겨준다, 컴파일러는 C 의 제어 구문을 구현하는데 이러한 낮은 수준 방법에 기초하여 인스트럭션 코드를 생성해야 한다
3.6.1 조건 코드
정수 레지스터들과 함께 CPU 는 최근 산술 또는 논리 연산의 특성을 설명하는 단일 비트 조건 코드로 구성된 레지스터들을 운영한다
이 레지스터들은 조건부 분기를 수행하기 위해서 시험될 수 있다
(* CPU 가 연산 결과를 추적하기 위해 플래그 (조건 코드) 를 사용하며, 이 플래그들을 통해 CPU 가 특정 조건에 따라 분기하거나 오류를 감지할 수 있다, 그리고 명령어에 따라 플래그 값이 달라질 수 있다)
CF - 캐리 플래그 Carry Flag, 가장 최근의 연산에서 가장 중요한 비트로부터 받아 올림이 발생한 것을 표시, 비부호형 연산에서 오버플로우를 검출할 때 사용
ZF - 영 플래그 Zero Flag, 가장 최근 연산의 결과가 0인 것을 표시
SF - 부호 플래그 Sign Flag, 가장 최근 연산이 음수로 생성한 것을 표시
OF - 오버플로우 플래그 Overflow Flag, 가장 최근 연산이 양수/ 음수의 2의 보수 오버플로우를 발생시킨 것을 표시
leaq 인스트럭션은 주소 계산에 사용하기 위한 것으로 조건 코드를 반영하지 않는다 (* 플래그에 영향을 주지 않는다)
반면에 아래 나열된 모든 인스트럭션들은 조건 코드 값을 변경한다 (* 플래그에 영향을 준다)
XOR 같은 논리 연산에서는 캐리와 오버플로우 플래그가 0으로 세팅된다
쉬프트 연산에서는 캐리 플래그가 쉬프트되어 없어지는 마지막 비트로 설정되며, 오버플로우 플래그는 0으로 세팅된다
그림 3.10 의 인스트럭션들에 의해 조건 코드 값이 변경될 뿐만 아니라, 다른 레지스터들은 변경시키지 않으면서 조건 코드만 변경해 주는 (8, 16, 32, 64비트 형식을 갖는) 두 개의 인스트럭션 클래스가 있다
CMP 인스트럭션들은 두 오퍼랜드의 차에 따라 조건 코드를 설정한다, 이들은 목적지를 갱신하지 않고 조건 코드를 설정한다는 점을 제외하고는 SUB 인스트럭션들은 같은 방법으로 동작한다 (* CMP 는 SUB 와 달리, 차이점을 저장하지 않고 플래그만 설정해준다)
ATT 형식에서 오퍼랜드들은 역순으로 나열되기 때문에 코드를 읽기가 어렵다 (* CMP A, B 이 B - A 을 의미한다)
이 인스트럭션들은 만약 두 오퍼랜드가 같으면 제로 플래그를 1로 설정한다, 다른 플래그들은 두 오퍼랜드의 순서 관계를 결정하는 데 사용될 수 있다
TEST 인스트럭션은 목적지 오퍼랜드를 변경하지 않으면서 조건 코드를 설정하는 점만 제외하고는 AND 인스트럭션과 같은 방식으로 동작한다, 일반적으로 같은 오퍼랜드가 반복되거나 (ex. %rax 가 음수인지 0인지 양수인지 알기 위해서는 testq %rax, %rax 와 같이 사용 * 자기 자신을 AND 연산하므로 결과값이 0이면 ZF 가 설정되고, 음수인 경우 SF 가 설정된다)
오퍼랜드 중의 하나는 시험할 비트를 가리키는 마스크이다 (*비트 마스크 - 어떤 값의 특정 비트가 1인지 확인하고 싶을 때 testq $0x1, %rax 라고 하면 %rax 의 마지막(제일 오른쪽, 최하위 비트) 비트가 1인 경우 결과가 1이 되고 ZF 는 0이 된다)
3.6.2 조건 코드 사용하기
조건 코드를 이용하는 보편적인 세 가지 방법이 있다
1) 조건 코드의 조합에 따라 0 또는 1을 한 개의 바이트에 기록하는 방법
아래 인스트럭션들은 클래스를 SET 인스트럭션이라고 부른다, 서로 다른 접미어를 가지며, 이에 따라 다른 조건 코드의 조합을 사용하여 서로 다른 동작을 한다 (* 주어진 조건에 따라 목적지 레지스터나 메모리 위치에 0 또는 1을 기록하는 명령어)
여기서 접미어가 오퍼랜드의 크기가 아닌 것에 주의 ex. setl, setb 는 set less, set below 를 의미한다
SET 인스트럭션은 목적지로 하위 단일 바이트 레지스터 가운데 한 개나 단일 바이트 메모리 주소를 사용하며, 이 바이트를 0이나 1로 기록한다, 32비트나 64비트 결과를 만들려면, 다른 상위 비트들을 0으로 만들어줘야 한다 (바이트 단위의 연산 결과를 더 큰 비트 폭에 맞추기 위해 상위 비트를 명시적으로 0으로 설정해야 됨)
cmpq 명령어는 두 값 a, b를 비교하고 결과에 따라 특정 플래그 (조건 코드) 가 설정된다
setl "값이 작으면 1로 설정" 을 사용하여 테스트하는 경우, 오버플로우가 발생하지 않았고 (OF가 0 값을 갖는 경우)
SF 비트가 1로 세팅되는 것으로 a < b 라는 것을 알 수 있다 (P195-196 책을 봐도 당최 이해가 안 가서 안 보려고 함 )
2) 조건에 따라 프로그램의 다른 부분으로 이동하는 방법
3) 조건에 따라 데이터를 전송하는 방법
3.6.3 점프 Jump 인스트럭션
점프 인스트럭션은 프로그램이 완전히 새로운 위치로 실행을 전환하도록 한다
점프의 목적지는 어셈블리 코드에서 레이블 Label 로 표시한다
목적 코드 파일을 만들기 위해 어셈블러는 모든 레이블이 붙은 인스트럭션들의 주소를 결정하고
점프 인스트럭션의 일부분인 "점프 목적지 Jump Target" (목적지 인스트럭션의 주소) 를 인코딩한다
점프 목적지가 인스트럭션의 일부로 인코딩되는 경우에는 직접 점프 (ex. jump .L1) 를
점프 대상을 레지스터나 메모리 위치로부터 읽어들여야 하는 경우에는 간접 점프를 사용한다
* 간접 점프는 '*' 와 메모리 오퍼랜드 중의 하나를 이용한 오퍼랜드 식별자를 합쳐서 작성한다
jmp *%rax - 레지스터 %rax 의 값을 점프 목적지로 사용
jmp *(%rax) - %rax 에 저장된 값을 읽기 주소로 사용하여 메모리에서 점프 목적지를 읽어 들인다
3.6.4 점프 인스트럭션 인코딩
점프를 인코딩하는 여러 가지 방법 중에 일반적인 방법은 PC 상대적 PC Relative 방법이다
대상 인스트럭션과 점프 인스트럭션 바로 다음에 오는 인스트럭션 주소와의 차이를 인코딩한다
두번째 인코딩 방법은 "절대" 주소를 제공하는 방법으로 대상을 직접 명시하기 위해 4바이트를 사용한다
어셈블러와 링커는 점프 목적지를 인코딩하는 방법을 적절히 선택한다
3.6.5 조건부 분기를 조건 제어로 구현하기
C에서 조건부 수식과 문장을 기계어 코드로 번역하는 가장 일반적인 방법은 조건부 및 무조건 점프를 사용하는 것이다
gotodiff_se 함수는 어셈블리 코드에서의 무조건 점프와 유사한 C의 goto 문을 사용한다
goto 문을 사용하는 것은 코드를 해독하고 디버깅하기 어렵게 할 수 있기 때문에 일반적으로 나쁜 프로그래밍 스타일이다
3.6.6 조건부 이동으로 조건부 분기 구현하기
조건부 동작을 구현하는 전형적인 방법은 조건이 만족되면 프로그램의 한 가지 실행 경로를 따르고,
아닌 경우에는 다른 경로를 따라가도록 하는 제어의 조건부 전환을 통해 이루어진다
또 다른 전략은 데이터의 조건부 전송을 이용하는 것이다
이 방법은 조건부 동작의 산출물 모두를 계산하고 조건에 따라 하나만 선택하는 방식이다, 이 전략은 제한적인 경우에만 의미를 갖지만, 최신 프로세서의 성능 특성과 잘 일치하는 조건부 이동 move 인스트럭션으로 구현할 수 있다
프로세서는 각 인스트럭션을 일련의 단계로 처리하며, 이 단계들은 각각 요구된 동작의 작은 부분만을 실행하는
파이프라인을 통해 높은 성능을 얻는다 (ex. 메모리로부터의 인스트럭션의 인출, 인스트럭션 타입의 결정 등)
이 방식은 예를 들면, 이전 인스트럭션의 산술 연산을 수행하는 동시에 다른 인스트럭션을 인출하는 것처럼 연속되는 인스트럭션의 산술 연산을 수행하는 동안에 다른 인스트럭션을 인출하는 것처럼 연속되는 인스트럭션들의 단계를 중첩시켜서 고성능을 얻는다
프로세서는 각 점프 인스트럭션이 실행될지를 추측하기 위한 복잡한 분기 예측 회로를 채택하고 있다
이를 안정적으로 예측할 수 있다면, 인스트럭션 파이프라인은 인스트럭션들로 가득 채워질 수 있지만
점프 하나를 잘못 예측하면, 미래의 인스트럭션을 위해 이미 실행한 작업 결과들을 상당 부분 버려야 하고,
정확한 위치에서 다시 인스트럭션들을 파이프라인에 채우는 작업을 수행해야 한다
일례로, absdiff 함수를 조건부 실행의 두 가지 방법을 사용하여 인텔 Haswell 프로세서에서 실행하고 시간을 측정하였을 때
전형적인 응용에서 x < y 테스트의 결과는 매우 예측하기가 어렵고,
함수의 실행 시간이 분기가 정확히 예측되었는지에 따라 8에서 27사이클을 소모한다
조건부 이동 명령을 사용해서 컴파일한 코드는 테스트하는 데이터와 상관 없이 약 8클럭 사이클을 필요로 한다
제어 흐름은 데이터와 관계 없고, 이것은 프로세서가 파이프라인을 꽉 찬 상태로 유지하는 것을 더욱 쉽게 해준다
3.6.7 반복문
C에서는 do-while, while, for 등 여러 가지 반복문 구문을 제공하지만, 기계어에는 여기에 대응되는 인스트럭션이 없다
대신에 조건부 테스트와 점프를 함께 사용해서 반복문의 효과를 구현한다
GCC와 다른 컴파일러들은 반복 코드를 . 두개의 기본 루프 패턴에 기초해서 반복문 코드를 생성한다
Do-While 반복문
반복문은 body-statement 를 반복적으로 실행하고, test-expr를 계산하여 그 결과가 0이 아니라면 반복 수행을 계속한다
이러한 일반 형식을 조건문과 goto문으로 변환하면 다음과 같고, 매 실행마다 프로그램은 본체 문장과 테스트 수식을 계산한다
While 루프
이 문장은 아래 goto 코드로 변환할 수 있다
While 문은 test-expr를 먼저 계산해서, body-statement를 실행하기 전에 종료될 수 있다
그림 3.20(a) 는 while 반복문을 사용하는 factorial 함수를 보여준다
옆에 있는 함수 fact_while_jm_goto는 GCC가 최적화를 위해
명령줄에 -0g 옵션을 이용해서 생성한 어셈블리 코드를 C코드로 해석한 것이다
그림 3.21은 그림 3.20의 Factorial 함수에 대한 동일한 C 코드이지만,
GCC를 명령어 옵션 -01 으로 실행했을 때 발생하는 컴파일 결과를 보여준다
그림 3.21(c) 는 실제 생성된 어셈블리 코드, 그림 3.21(b) 는 보다 읽기 쉬운 C 표현으로 만들어 준다
'크래프톤정글 > CS:APP' 카테고리의 다른 글
CH06 메모리 계층구조 The Memory Hierarchy (0) | 2024.10.25 |
---|---|
[CS:APP] CH09 가상메모리 - 9.9, 9.11 (3) | 2024.10.20 |
CH07 링커 Linking (1) | 2024.10.14 |
CH03 프로그램의 기계 수준 표현 3.1-3.5 (7) | 2024.09.27 |
CH01 컴퓨터 시스템으로의 여행 (7) | 2024.09.14 |