csapp 3.6이다
마음 같아선 3.11 까지 세번 완독하고
백덤블링도 3바퀴 돌았는데
안타깝게도 몸이 따라주지 못해
오늘은 csapp 3.6까지만 하겠다
이번에는 C언어에서 if문 loop문 같이 조건에 따라
실행 흐름이 달라지는 구조에 대해서 알아볼거다
cpu에 최근 수행된 산술 또는 논리 연산의 특성을 나타내는 비트가 있는데 조건 분기 사용할 때 쓴다
아래에는 유용한 조건 코드들이다
leaq
명령어는 주소만 계산해서 조건 코드 건드리지 않음
연산 결과의 특성을 나타내는 조건 코드(CF, ZF, SF, OF)는 분기나 조건 명령의 핵심 기준이다
조건 코드는 보통 직접 읽거나 하다기 보다는
3가지 방법으로 관리한다
조건 코드 조합에 따라 1바이트 값을 0 또는 1로 설정
조건에 따라 프로그램의 다른 위치로 점프(분기) 수행
조건에 따라 데이터 이동 수행
첫번째 경우로 조건 코드 값(예: ZF, SF 등)에 따라, 1바이트 레지스터나 메모리 위치에 0또는 1을 저장한다
보통 C의 비교 연산이나 연산 결과를 bool
값으로 사용하고 싶을때 쓴다
예시:
sete %al
($ZF=1$일 때 %al
에 1 저장, 아니면 0)
setl %al
($SF^{OF}=1$일 때 %al
에 1 저장, 아니면 0)
조건 코드에 따라 프로그램 실행 흐름(PC)를 다른 위치로 분기한다
주로 if, while 등 C의 제어문 구현에 쓰인다
예시:
je label
($ZF=1$이면 label
로 점프)
jl label
($SF^{OF}=1$이면 label
로 점프)
조건 코드의 값에 따라 값을 복사(이동)할지 말지 결정하는 명령어(cmov, cmovz, cmovl 등)
분기하는 대신 전부 계산 후 조건에 따라 두 값중 하나 선택
분기 예측 실패 예방 ㄱㄴ
예시:
cmovge %rdx, %rax
(x >= y일 때만 %rdx
값을 %rax
에 복사)조건 코드 값에 따라 1/0을 저장하는 set 명령, 조건부 점프, 조건부 데이터 이동이 구현된다
조건 코드와 더불어, 점프 명령어는 프로그램의 실행 흐름(PC)을 변경하는 핵심 도구다
조건부와 무조건 점프를 적절히 사용해 C의 if
/else
, 반복문 등 제어 구조가 어셈블리로 구현된다
조건 없이 무조건 지정한 위치(레이블)로 실행 흐름을 이동한다
함수 호출 전후, 루프 진입/탈출 등에서 활용
예시:
jmp label
(바로 label로 이동)
jmp *%rax
(레지스터 값이 가리키는 주소로 이동, 간접점프)
조건 코드 값에 따라 특정 레이블로 분기한다
C의 if
, while
, for
, switch
등 조건 제어 구현의 기본 도구
다양한 조건(==, !=, <, > 등)에 따라 jump 명령어가 준비되어 있음
예시:
je label
($ZF=1$일 때 label로 점프, Equal/Zero)
jne label
($ZF=0$일 때 label로 점프, Not Equal)
jl label
($SF^{OF}=1$일 때 label로 점프, Less Than)
jg label
($(SF^{OF}=0$이고 $ZF=0$)일 때 label로 점프, Greater Than)
직접 점프: jump 대상이 어셈블리 코드의 레이블로 직접 지정
jmp .L1
간접 점프: jump 대상이 레지스터나 메모리의 값(주소)
jmp *%rax
, jmp *(%rsp)
조건부/무조건 점프 명령어로 프로그램 흐름을 바꿀 수 있다
기계어에서 점프 명령어(jmp, je, jne 등)는 점프 대상 주소(어디로 이동할지)를 반드시 명령어 내부에 인코딩해야 한다
이때 주로 사용하는 방법이 PC(Program Counter)-상대 오프셋 방식이다
즉,
점프 명령어 다음 명령어의 주소와 점프 타겟 명령어의 주소의 차이(오프셋)를 저장한다.
이 오프셋은 1, 2, 4 바이트 등 다양한 길이로 저장 가능하며, 프로그램이 메모리 어디에 적재되든 인코딩이 변하지 않는다.
절대 주소(Absolute address) 방식도 있지만, 대부분은 PC-relative 방식이 쓰인다.
예시)
jmp .L2 # .L2까지 몇 바이트 건너뛸지 오프셋으로 저장
실제 기계어 바이트에서는, jmp
다음 바이트에 오프셋이 저장됨
이 오프셋 값 + “다음 명령어 주소” = 점프 목적지
(실제 예: 0x3에서 eb 03 ⇒ 3 + 3 = 0x6, 즉 0x8 주소로 점프)
점프 명령의 대상 주소는 주로 PC-상대 오프셋(다음 명령 기준)으로 인코딩된다
여기서 부턴 좀 간략히 쓰겠다
피곤하고 눈 아파서 괴롭다
조건문(if-else)은 조건 검사와 분기(jump) 명령의 조합으로 구현된다
C의 if-else, switch 등 조건문은 결국 비교 명령어(cmp 등)와 조건부 점프 명령어(jge, jl 등)를 이용해 각 분기(then/else 블록)로 흐름을 나눈다
어셈블리에서의 분기 구조는 C의 goto 문과 매우 비슷하며, 실제 실행 흐름이 눈에 보이도록 나타난다
조건부 데이터 이동(cmov)은 분기 없이, 조건에 따라 값만 선택할 때 효과적이다
분기 대신 cmov 명령어를 사용하면, 비교 결과에 따라 특정 레지스터 값만 덮어쓸 수 있다
파이프라인 효율이 좋아지고 분기 예측 실패에 따른 성능 손실이 없으나, 부작용/에러 가능성이 있는 코드는 사용에 주의해야 한다
주로 if문의 각 블록이 간단한 대입일 때, 그리고 양쪽 결과를 모두 미리 계산해도 문제가 없을 때 활용된다
C의 반복문(while, do-while, for)은 기계어에서는 조건 검사와 점프 명령의 조합으로 모두 구현된다
컴파일러는 코드 구조와 최적화 수준에 따라 다양한 루프 패턴(jump-to-middle, guarded-do 등)으로 변환한다
do {
body;
} while (test);
동작:
goto 변환:
loop:
body
if (test) goto loop;
기계어:
while (test) {
body;
}
jump-to-middle 방식:
goto test;
loop:
body
test:
if (test) goto loop;
guarded-do 방식:
if (!test) goto done;
loop:
body
if (test) goto loop;
done:
for (init; test; update)
body;
init;
while (test) {
body;
update;
}
init;
goto test;
loop:
body;
update;
test:
if (test) goto loop;
반복문(while, do-while, for)은 조건 검사와 점프 명령의 조합으로 기계어로 구현된다
3.6.1 CPU는 연산 결과마다 캐리(CF), 제로(ZF), 부호(SF), 오버플로우(OF) 등의 1비트 조건 코드를 남긴다. 이후 조건 분기, 비교, set 명령 등에서 이 값을 사용한다.
3.6.2 set 계열 명령어(sete, setl 등)는 조건 코드에 따라 바이트 값을 0/1로 저장, 조건부 분기(jump), 조건부 데이터 이동에도 활용된다.
3.6.3 무조건(jmp) 및 조건부(je, jne, jg 등) jump 명령어를 통해 코드 실행 위치를 동적으로 바꿀 수 있다. 조건부 jump는 주로 비교 이후 조건 코드 조합을 사용한다.
3.6.4 점프 주소는 명령어 포인터(PC) 기준 상대 오프셋으로 주로 저장되어, 프로그램의 재배치 및 효율적 실행이 가능하다.
3.6.5 if-else문은 비교 후 조건부/무조건 점프로 흐름을 나누어 구현한다. C의 goto 코드와 어셈블리 분기 구조가 거의 일치한다.
3.6.6 cmov 명령 등 조건부 이동은 분기 대신 조건에 따라 값만 선택하며, 분기 예측 실패 시 페널티 없이 효율적으로 처리할 수 있다. 단, 양쪽 결과를 항상 계산하므로 부작용/에러 가능성이 있는 경우 사용하지 않는다.
3.6.7 반복문은 본문(body) 실행, 조건 검사, 점프를 조합해 구현하며, do-while/while/for 모두 jump-to-middle, guarded-do 등 패턴으로 기계어로 변환된다.