1의 보수: 모든 비트를 반전시킨 값으로, 각 비트에 대해 0은 1로, 1은 0으로 변경한다. 1의 보수는 +0과 -0의 두 가지 표현을 갖는다는 단점이 있다.
2의 보수: 1의 보수에 1을 더한 값으로, 가장 널리 사용되는 음수 표현 방식이다. 2의 보수는 오버플로를 자연스럽게 처리할 수 있으며, +0과 -0을 단 하나의 0으로 표현한다는 장점이 있다. 이는 산술 연산을 더 간단하게 만들어 준다.
unsign연산 / sign연산
C/C++ 프로그래밍에서 unsigned는 부호가 없는 즉 음수를 표현하지 않겠다 라는 의미이고 signed는 부호가 있는, 즉 음수로 표현이 가능하다라는 의미 이다.
CF(Carry Flag) : 가장 최근의 연산에서 가장 중요한 비트로부터 올림이 발생한 것을 표시한다.
-> 비부호형 연산(unsigned)에서 오버플로우를 검출
1111 + 0001 = 0000
ZF(Zero Flag) : 가장 최근 연산의 결과가 0인것을 표시
0111 + 0001 = 1000
1000 - 0001 = 0001
SF(Sign Flag) : 가장 최근 연산이 음수를 생성한 것을 표시
0000 - 0001 = 0000
OF(Overflow Flag) : 가장 최근 연산이 2의 보수 오버플로우를 발생시킨 것을 표시
-> 부호형 연산(signed)에서 오버플로우를 검출
0100 + 0100 = 1000
인스트럭션은 컴퓨터에게 일을 시키는 단위로서 컴퓨터가 알아들을 수 있는 기계어로 이루어져 있는 명령어이다.
기계코드는 명령이 순차적으로 실행되는데 조건부에 걸쳐 jump를 할 수 있다.
-점프 인스트럭션 : 시험 결과에 따라서 프로그램의 다른 일부분으로 제어를 넘긴다.
-명령부(OP) : 실제 컴퓨터가 수행해야 할 동작을 나타냄, 주로 산술 및 논리연산, 데이터 이동, 분기, 입출력, 그 외의 제어 명령 ex) 위 사진에서의 mov
-처리부(operand) : 동작의 대상이 되는 데이터를 지정, 피연산자를 직접 나타내거나 피연산자가 기억된 레지스터 또는 기억장치의 주소이다. ex) 위 사진에서의 eax
mov eax, ecx 라는 명령어 한 줄 중에 eax가 1오퍼랜드, ecx가 2오퍼랜드 mov가 op이다.
mov(move) : 좌변에 우변의 값을 복사 (우변에 연산자가 올 수 없음)
mov eax, ecx+10
>
add ecx, 10
mov eax, ecx
하지만 주소를 나타내는 mov eax,[ebo+4]같은 연산은 가능하다. 예를 들어 ebp 레지스터가 100번지 주소를 저장하고 있고 104번지에 20이라는 값이 있다면 eax엔 20이라는 값이 저장된다.
lea(load effective address) : 좌변에 우변의 주소값을 저장 (좌변에 레지스터만 올 수 있음)
leaq, movq // 둘이 연산식이 같음
임의의 레지스터 안에 있는 값 + 임의의 레지스터 안에 잇는 주솟값 * index에 곱해줄 2,4,8중 하나의 정수 + 8, 16, 32 bit 중 하나의 값
둘 다 메모리 참조 없이 주소를 계산할 수 있는 명령어지만, movq는 계산 후 메모리에 접근하여 메모리에 저장된 값을 가져온다는 의미, 반면에 leaq는 계산한 값을 취하기만 할 뿐 메모리에는 접근하지 않음
다른 레지스터들은 변경시키지 않으면서 조건 코드만 변경해 주는 두개의 인스트럭션 : CMP, TEST
CMP(compare) 명령어는 두 가지 피연산자의 차이에 따라 조건 코드를 설정한다. (두 피연산자를 비교)
두 피연산자의 값이 값이 같다면 결과는 0이 되고 ZF가 1으로 세트된다. 값이 다르면 ZF가 0으로 세트된다.
SUB 명령어와 같은 방식으로 작동하지만, 그들의 목적지를 업데이트하지 않고 조건 코드를 설정하는 것은 다르다. (SUB과 비슷하지만 값을 바꾸진 않음)
TEST 명령어는 AND 명령어와 같은 방식으로 작동하지만, 그들의 목적지를 변경하지 않고 조건 코드를 설정하는 것은 다르다.
조건 코드를 직접적으로 읽는 거 보다, 조건 코드를 사용하는 데 세 가지 흔한 방법이 있다.
1. 조건 코드의 어떤 조합에 의존하여 단일 바이트를 0또는 1로 설정한다.
2. 다른 프로그램으로 조건적으로 jump할 수 있다.
3. 데이터를 조건적으로 전송할 수 있다.
SET 인스트럭션은 목적지로 하나의 '단일바이트' 레지스터나, '단일바이트'메모리주소를 사용한다. 이 바이트를 0이나 1로 기록할 것이기 때문에 굳이 큰 바이트가 필요할까 싶다.
각 명령어들은 조건 코드의 조합을 기반으로 단일 바이트를 0 또는 1로 지정한다.
어떤 명령어는 "synonyms"를 가지고 있으며, 이는 같은 기계 명령어의 대안 이름이다.
L 동의어 (setl set less가 아닌 set long으로 정하는건 컴퓨터가 알아서 랜덤으로 판단)
jump 명령어는 프로그램에 완전히 새로운 위치로 바꾸어 실행한다.
jump 목적지는 label(라벨)로 어셈블리 코드 내를 가리킨다.
jump .L1 명령어는 밑에 movq 명령어를 skip하고popq 명령어로 재개한다. 오브젝트-코드 파일을 생성하여, 어셈블러는 모든 라벨된(labeled) 명령어의 주소를 결정하고 jump 타겟을 jump 명령어의 한 부분으로 인코딩한다.
jmp *%rax는 레지스터 내의 값인 %rax를 jump 타겟으로 사용하고, jmp *(%rax)는 메모리로부터 jump 타겟을 읽는다. 어떻게 jump 명령어의 타겟이 인코딩되는지를 이해하는 것은 링킹(linking)에서 중요한 부분이다. jump 타겟은 상징적 라벨을 사용해서 작성된다.
간접 점프의 예:
jmp *%rax -> 레지스터 rax의 값을 점프 목적지로 사용한다.
jmp *(%rax) -> 레지트터 rax에 저장된 값을 주소로 하는 메모리값을 점프목적지로 읽는다.
어셈블러와 링커는 jump 타겟의 적절한 인코딩을 생성한다.
많은 인코딩이 있지만, 가장 흔하게 사용하는 것은 PC relative(PC 상대적 방식)이다. 점프목적지는 심벌레이블을 사용해서 작성한다. 어셈블러와 링커는 점프 목적지를 적절히 인코딩한다. 대상 인스트럭션과 점프 인스트럭션 바로 다음에 나오는 인스트럭션 주소와의 차이를 인코딩한다.
타겟 명령어의 주소와 jump을 따라가는 명령어의 주소 간의 차이를 인코딩한다. 두 번째 인코딩 방법은 "절대적인" 주소를 주는 것으로 타겟을 바로 구체화하기 위해4 바이트를 사용한다. PC-relative 어드레싱을 수행할 때 Program Counter의 값은 jump 자체가 아닌 jump을 따르는 명령어의 주소이다.
링킹 이후에 프로그램에서 명령어는 다른 주소로 재배치 받지만 jump 타겟은 바뀌지 않는다. C에서 조건문과 명령어를 기계 코드로 번역하는 가장 일반적인 방법은 조건적인 jump와 무조건적인 jump의 조합을 사용하는 것이다.(PC상대 방식으로 점프 목적지를 인코딩하면, 인스트럭션들이 간결하게 인코딩되고 목적코드는 수정없이 메모리상의 다른 위치로 이동될수 있다고 한다. // 역추적도 가능)
조건 연산을 구현하기 위한 가장 관습적인 방법은 제어의 조건적 전송을 통한 구현이다. 이 방법은 매우 단순하고 일반적이지만, 모던 프로세서에서는 비효율적이다. 대안적인 전략은 데이터의 조건적 전송을 통해 구현하는 것이다. 이 접근은 조건 연산의 결과와 조건에 속하는 것을 하나 선택하여 계산한다. 이 전략은 매우 제한적인 경우지만, 간단한 조건적 이동 명령어에 의해 구현된다.
조건적 데이터 전송에 따른 코드가 조건적 제어 전송보다 성능이 좋은 지 이해하려면, 모던 프로세서 연산을 이해해야 한다. 프로세서는 파이프라이닝(pipelining)을 통해 높은 성능을 얻는다.
최신 프로세서들은 파이프라인을 통해 높은 성능을 얻는데, 프로세서가 조건부 점프를 만나게 되면, 프로세서는 분기 조건에 대한 계산이 완료될때까지 어느쪽으로 분기될지 결정을 할 수 없다. 즉 조건계산이 될때까지 기다려야한다. 만약에 조건식 계산을 빠르게 예측가능하다면, 조건제어가 더 빠를 수 있겠지만, 그걸 예측하는거는 힘들다고 한다. 조건부 이동명령을 이용하여 컴파일을 하면 적당한 사이클의 시간이 걸리기때문에 사용하기에 좋다. 이 방식은 프로세서가 파이프라인을 꽉 찬 상태로 유지하는것을 더 쉽게 해준다.
C는 do-while, while, for문과 같은 다양한 반복문을 제공한다. 기계 코드에는 대응하는 명령어가 없지만, test와 jump의 조합으로 반복문을 구현한다.
1. Do-While Loops
2. While Loops
3. For Loops
조건부 테스트와 점프를 적절히 사용해서 구현해야한다. 이 부분에서 do-while, while, for에 대한 각각의 어셈블리코드는 책에 예제 코드가 잘 나와있다. 결국에 이 모든걸 jump로 구현할 수 있다.
Switch문은 정수 인덱스 값에 따라 다중분기 기능을 제공한다. 테스트해야하는 경우의 수가 많은 경우에 특히 유용하다. 점프 테이블이라는 자료구조를 사용해서 효율적인 구현이 가능하다
점프테이블은 switch문의 인덱스가 i일때, 프로그램이 실행해야 하는 동작을 구현하는 코드 블록의 주소가 되는 배열을 말한다. if-else문을 사용하는 것보다 switch 문을 사용하는것이, case의 수에 관계없이 일정하다는 것이 장점이다.
a에서 주목해야할것은 case102와 104106이다.
b에서는 들어오는 n값에서 100을 뺀것을 인덱스로 취하는 점프테이블을 사용한다.
점프테이블에서, 비어있는 인덱스는 loc_def로 가게끔 해놓은 것이 인상깊다.
어셈블리 코드를 살펴보면 점프테이블로 가는 간접점프가 나온다. %rsi에 인덱스를 저장하여 사용된다.
점프테이블 또한 어셈블리 코드로 번역된다.
자세하게는 알것 없고, 맨위에 써있는 L4가 앞서 점프테이블 간접점프에서 쓰였는데, 이는 이 레이블의 시작주소로 활용된 것이다. 이렇게 점프테이블을 이용하면 다중분기를 매우 효율적인 방식으로 구현할 수 있게된다.
'CS > 어셈블리어' 카테고리의 다른 글
프로시저란 무엇인가? (0) | 2024.03.21 |
---|