Fundamental of CS/: : CSAPP

[CSAPP] Ch 3. 프로그램의 기계수준 표현 : (6) 제어문

Jay.P Morgan 2024. 3. 19. 23:24

  3.6  제어문

 

  3.6.1  조건코드

 

  앞서 보았듯, CPU에는 정수 레지스터와 함께 가장 최근의 산술 또는 논리연산의 특성을 설명하는 레지스터들을 운영한다.

CF: 캐리플래그(carry flag). 가장 최근의 연산에서 가장 중요한 비트로부터 올림이 발생한 것을 표시

비부호형 연산에서 오버플로우를 검출

ZF: 영플래그 (Zero flag). 가장 최근 연산의 결과가 0인것을 표시

SF: 부호 플래그 (Sign flag). 가장 최근 연산이 음수를 생성한것을 표시

OF: 오버플로우 플래그 (Overflow flag). 가장 최근 연산이 2 보수 오버플로우를 발생시킨 것을 표시

부호형 연산에서 오버플로우를 검출

​​

다른 레지스터들은 변경시키지 않으면서 조건 코드만 변경해 주는 두 개의 인스트럭션이 있다.

 

 

  CMP인스트럭션은 오퍼랜드의 차에 따라 조건코드를 실행한다. sub 유사한 형태이지만, 값을 바꾸진 않는다.

cmp S1, S2 진행했을때, S2-S1 0이면 ( S1 = S2) 영플래그를 1 설정한다.

 

 

  위와같이 다른 플래그들도 오퍼랜드의 순서관계를 결정하는데 이렇게 쓰일 있다.

​  TEST 인스트럭션은 차이로 조건코드를 설정하는게 아니라, AND 연산으로 테스트를 진행한다. AND연산과 유사한 형태지만, 역시 값을 바꾸진 않는다.

  예를들어 %rax 음수인지, 0인지, 양수인지 알기위해서 testq %rax, %rax 통해 알아낼 있다.

 

 

  3.6.2  조건코드 사용하기

 

 

  위 조건코드에서 접미어를 보면, 이것이 오퍼랜드의 크기를 나타내는 것이 아니다. 조건코드의 어떤 조합을 사용할 것인지를 나타낸다. 예로, setl set less 나타낸다. (setl에서의 l long 아님!)

  위에서 Instruction 대해 Synonym(동의어) 존재하는데, 컴파일러가 알아서 뭘쓸지 랜덤으로 결정한다.

​  SET 인스트럭션은 목적지로 하나의 '단일바이트' 레지스터나, '단일바이트'메모리주소를 사용한다. 바이트를 0이나 1 기록할 것이기 때문에 굳이 바이트가 필요할까 싶다.

 

  마지막에 %al값을 큰데로 옮기고 싶으면 확장해서 옮기면 된다.

재미난 주제: OF SF 이용하여 부호형 a,b 크기 비교하기

  만약 오버플로우가 발생하지 않았다면 OF == 0이다. 이때는 크게 문제될게 없다. SF a-b<0이면 1값이고, 그외는 0값이다그래서 OF==0일떄는 SF==1이면 a<b이고, SF==0이면 a>b이다.

  하지만, 만일 오버플로우가 났다고 생각해보자. OF==1이다이때 문제가 된다. 오버플로우가 나면 실제로 a-b<0인데, 값은 양수로 뜬다. 반대로는, 실제는 a-b >0인데, 값은 음수로 뜬다 (부호가 뒤바뀜). 그래서 OF==1일때는 반대로 생각해줘야한다.

  OF==1일때 SF==1이면 a>b이고 SF==0이면 a<b이다.

  비부호형의 비교에서는 OF대신 CF 오버플로우를 감지하는 역할을 한다.

 

 

  3.6.3  점프 인스트럭션

 

  일반적인 실행에서는, 인스트럭션들이 나열된 순서에 따라 실행된다하지만 점프 인스트럭션은 프로그램이 완전히 새로운 위치로 실행을 전환할 있게끔 해준다.

 

  목적 코드파일을 만들기 위해서 어셈블러는 모든 레이블이 붙은 인스트럭션들의 주소를 결정하고, 점프 인스트럭션의 일부분인 점프목적지를 인코딩해야 한다.

  점프목적지가 인스트럭션의 일부로 인코딩되는 경우는 "직접점프"(위의 예시), 점프 대상을 레지스터나 메모리 위치로부터 읽어들여야하는 경우에는 "간접점프" 사용한다.

 

간접 점프의 :

jmp *%rax -> 레지스터 rax 값을 점프 목적지로 사용한다.

jmp *(%rax) -> 레지트터 rax 저장된 값을 주소로 하는 메모리값을 점프목적지로 읽는다.

아래 나열되어있는 것들은 "조건부 점프" 이다. j뒤에 접미사로 달리는 조건을 만족해야 점프한다.

 

 

 

  3.6.4  점프 인스트럭션 인코딩

 

  점프목적지는 심벌레이블을 사용해서 작성한다. 어셈블러와 링커는 점프 목적지를 적절히 인코딩한다가장 일반적인 방식은 "PC 상대적" 방식이다. 대상 인스트럭션과 점프 인스트럭션 바로 다음에 나오는 인스트럭션 주소와의 차이를 인코딩한다.

  두번째방식은 "절대주소" 방식이다. 대상을 직접 명시하기 위해 4바이트를 사용한다.

  PC상대적 방식에서는 오른쪽과 같이 점프 목적지가 계산된다. jmp인스트럭션의 주소는 0x03이고 다음 인스트럭션의 값이 5:라서  값을 더하면 점프 목적지 주소인 0x8 얻는다.

  마찬가지로 jg인스트럭션의 주소는 f8(십진수로 -8)이고다음 인스트럭션은 0xd(십진수로 13)이므로, 둘을 더하면 0x5 나온다. (암기)

  PC상대 방식으로 점프 목적지를 인코딩하면, 인스트럭션들이 간결하게 인코딩되고 목적코드는 수정없이 메모리상의 다른 위치로 이동될수 있다는 장점이 있. 여기서 PC 상대적 방식이 인스트럭션 주소의 상댓값만 가지고도 이렇게 주소를 역추적할 있다는 것을 있다.

 

  3.6.5  조건부 분기를 조건제어로 구현하기

 

  C언어에서 조건문 문장을 기계어 코드로 번역하는 가장 일반적인 방법은 조건부 무조건 점프를 함께 사용하는 것이다. 이는 "조건제어" 이용하는 방식이다.

  goto문은 좋은 프로그래밍 아니지만, 어셈블리 코드의 제어흐름을 설명하기에는 적절하다.

※ jump문은 goto문과 어셈블리 수준에서 굉장히 유사함을 보여준다.

 

  if else 이루어진 C코드를, goto문으로 바꿨을때 이해할 있어야한다.

 

 

  3.6.6  조건부 이동으로 조건부 분기 구현하기

 

※ rval과 eval을 모두 계산해두고, 마지막에 고른다

 

  여러 경우의 값들을 미리 계산하고 마지막에 택하는 방식의 장점:

 

  최신 프로세서들은 파이프라인을 통해 높은 성능을 얻는데, 프로세서가 조건부 점프를 만나게 되면 프로세서는 분기 조건에 대한 계산이 완료될때까지 어느쪽으로 분기될지 결정을 없다. 조건계산이 될때까지 기다려야한다.

 

  만약에 조건식 계산을 빠르게 예측가능하다면 조건제어가 빠를 있겠지만, 그걸 예측하는 것은 힘들다. 조건부 이동명령을 이용하여 컴파일을 하면 적당한 사이클의 시간이 걸리기때문에 사용하기에 좋다.

  이 방식은 프로세서가 파이프라인을 상태로 유지하는것을 쉽게 해주어 효율적이다.

 

※ 조건부 이동 인스트럭션

 

  조건부 이동의 인스트럭션이다. 두 개의 오퍼랜드를 가지며 소스에는 레지스터, 메모리위치 가능목적지는 레지스터가 가능하다. 소스값으로는 메모리나 소스 레지스터로부터 읽어오지만, 목적지에 값이 복사되기 위해서는 명시된 조건이 만족 되어야만 한다.

  소스와 목적지는 16비트 이상의 길이만 가능하다 (단일 바이트는 안됨). 어셈블러가 목적지 레지스터의 이름으로부터 오퍼랜드의 길이를 추정하기 떄문에, 크기접미사에 자유롭다.

  그렇다면 조건부 이동이 항상 조건제어보다 항상 좋을까조건부 이동이 아예 안먹히는 방식도 존재한다.

※  포인터가 null일 때 결과를 0으로 저장하는 코드

 

  위의 어셈블리 코드에서 xp 역참조에 실패한 경우가 발생하면 역참조 에러를 발생시킨다이러면 코드는 아예 쓸수가 없어진다또한 조건부이동은 모든 경우의 케이스를 미리 계산하는데, 계산양이 상당하다면 비용이 너무 크게 발생된다. 무언가 하나는 아예 안쓸것이기 때문이다.

 

 

  3.6.7  반복문

 

  반복문에는 크게 do-while, while 그리고 for문이 있다아쉽게도 각각에 대응되는 어셈블리어는 없다. 조건부 테스트와 점프를 적절히 사용해서 구현해야한다. (결국에는 모든걸 jump 구현할 있다)

do-while → body-statement 적어도 한번은 실행된다.

 

※ do-while문은 완전히 goto문으로 표현이 가능하다

 

while  body-statement 한번도 실행되지 않을 있다.

※ while문을 goto문으로 바꾼 두 번째 방식

 

for 루프문    결국 While문으로 치환이 가능하다.

 

 

 

 

Switch문은 정수 인덱스 값에 따라 다중분기 기능을 제공한다. 테스트해야하는 경우의 수가 많은 경우에 특히 유용하다. 점프 테이블이라는 자료구조를 사용해서 효율적인 구현이 가능하다~

점프테이블은 switch문의 인덱스가 i일때, 프로그램이 실행해야 하는 동작을 구현하는 코드 블록의 주소가 되는 배열을 말한다. if-else문을 사용하는 것보다 switch 문을 사용하는것이, case 수에 관계없이 일정하다는 것이 장점이다.

 

 

  3.6.8  switch 문

 

  Switch문은 정수 인덱스 값에 따라 다중분기 기능을 제공한다. 테스트해야하는 경우의 수가 많은 경우에 특히 유용하다. 점프 테이블이라는 자료구조를 사용해서 효율적인 구현이 가능하다. 점프테이블은 switch문의 인덱스가 i일때, 프로그램이 실행해야 하는 동작을 구현하는 코드 블록의 주소가 되는 배열을 말한다.

  if-else문을 사용하는 것보다 switch 문을 사용하는것이, case 수에 관계없이 일정하다는 것이 장점이다.

 

  a에서 주목해야할것은 case102 104106이다. b에서는 들어오는 n값에서 100 뺀것을 인덱스로 취하는 점프테이블을 사용한다.

  점프테이블에서, 비어있는 인덱스는 loc_def 가게끔 해놓은 것이 인상깊다.

 

 

  어셈블리 코드를 살펴보면 점프테이블로 가는 간접점프가 나온다. %rsi 인덱스를 저장하여 사용된다.

점프테이블 또한 어셈블리 코드로 번역된다.

 

 

  맨위에 써있는 L4 앞서 점프테이블 간접점프에서 쓰였는데, 이는 레이블의 시작주소로 활용된 것이다. 이렇게 점프테이블을 이용하면 다중분기를 매우 효율적인 방식으로 구현할 있게된다.