
3주차는 알고리즘 마지막 주차이자 c언어로 자료구조를 다루기전에 준비하는 주차이다.
커널의 역할과 64비트 메모리 사용 (64비트 시스템에서)
gcc로 컴파일만 해주면 코드를 재활용 할 수 있다.컴파일러 최적화에 대해 이해할 수 있음.
// C 코드
int sum(int a, int b) {
return a + b;
}
// 어셈블리 코드 (x86_64)
sum:
mov eax, edi
add eax, esi
ret
반복된 컴파일 과정 속에서 나오는 어셈블리의 성능을 측정해 성능을 극대화할 수 있다.
// C 코드
for (int i = 0; i < 1000; i++) {
array[i] = i * 2;
}
// ; 어셈블리 코드 (x86_64)
loop:
mov eax, ecx
shl eax, 1
mov [rdi + rcx*4], eax
inc ecx
cmp ecx, 1000
jl loop
i * 2가 어셈블리에서 shl eax, 1로 최적화되어있다. 어셈블리 코드를 보면, 시프트 연산이 곱셈보다 더 빠르다는 것을 알 수 있는데 이를 통해 성능을 극대화할 수 있다.
고급언어가 추상화하는 프로그램의 런타임 행동을 이해할 수 있다.
// C 코드
int factorial(int n) {
if (n <= 1) return 1;
else return n * factorial(n - 1);
}
// ; 어셈블리 코드 (x86_64)
factorial:
cmp edi, 1 ; (1) n <= 1인지 비교
jle .L2 ; (2) 조건이 참이면 .L2로 점프
sub edi, 1 ; (3) n = n - 1
call factorial ; (4) 재귀 호출
imul eax, edi ; (5) n * factorial(n - 1)
add edi, 1 ; (6) n을 원래 값으로 복원
ret ; (7) 반환
.L2:
mov eax, 1 ; (8) n <= 1인 경우 1을 반환
ret ; (9) 반환
n을 저장하는 레지스터재귀 호출의 동작:
메모리 관리:
n의 값을 저장하고, 재귀 호출을 위해 값을 감소시킨 후 다시 원래 값으로 복원하는 과정을 통해 메모리 관리의 기본적인 개념을 이해할 수 있다레지스터 사용:
고급 언어의 쓰레드 패키지를 통해 동시 프로그래밍을 할 때 공유 자원이 오가는 것을 머신 레벨에서 파악 가능.
// C 코드 (Mutex를 사용한 예시)
pthread_mutex_t lock;
void *increment(void *arg) {
pthread_mutex_lock(&lock);
count++;
pthread_mutex_unlock(&lock);
return NULL;
}
//; 어셈블리 코드 (x86_64)
increment:
push rbp
mov rbp, rsp
mov rdi, lock
call pthread_mutex_lock
mov rax, count
add rax, 1
mov count, rax
mov rdi, lock
call pthread_mutex_unlock
pop rbp
ret
프로그램의 취약점을 방어할 수 있다.
// C 코드 (취약한 코드)
void vulnerable_function(char *str) {
char buffer[10];
strcpy(buffer, str);
}
; 어셈블리 코드 (x86_64)
vulnerable_function:
push rbp
mov rbp, rsp
sub rsp, 16
mov rdi, buffer
call strcpy
leave
ret
이 예제에서 strcpy를 사용한 버퍼 오버플로우 취약점을 볼 수 있습니다. 어셈블리 코드를 보면, buffer가 스택에 할당되고 strcpy가 호출되며 버퍼 크기를 넘는 데이터가 쓰여질 수 있음을 알 수 있습니다. 이를 통해 스택 보호 기법(예: 스택 캔어리)을 이해하고 적용할 수 있습니다.
어셈블리 언어를 배우면 컴파일러 최적화, 성능 측정 및 최적화, 프로그램의 런타임 행동, 동시 프로그래밍의 동기화 문제, 그리고 프로그램 취약점의 방어 등에 대해 깊이 이해할 수 있고 이러한 지식은 고급 언어 프로그래밍을 더욱 효과적이고 안전하게 만드는 데 큰 도움이 된다.
O0(최적화 없음), O1, O2, O3(최고 수준 최적화), 그리고 디버깅을 염두에 둔 Og가 있다.Og는 성능 향상과 함께 디버깅 정보를 유지하여 디버깅이 용이하도록 한다.S 옵션은 컴파일러가 소스 코드를 어셈블리 코드로 변환한 후, 이를 실행 파일로 링크하지 않고 어셈블리 코드 상태로 남겨둠파일명.s 파일은 주어진 C 소스 코드의 어셈블리 버전을 담고 있게 됨따라서, gcc -Og -S asdad.s 명령어를 실행하면, GCC는 다음과 같은 작업을 수행
Og 옵션에 따라 디버깅을 위한 적절한 최적화를 수행파일명.s 파일로 저장책에서는 gcc, x86_64 기준으로 설명하고 있음. gcc 버전에 따라 어셈코드가 달라 보일 수 있음
소스코드를 기계가 이해할 수 있는 언어로 변환하는 과정 : 전처리 → 컴파일 → 링킹
MOV는 데이터를 이동시키는 명령어를 의미한다.가상 주소는 운영체제가 관리하며, 프로그램이 직접 물리적 메모리에 접근하는 것을 방지하고 메모리 보호, 주소 공간 분리, 효율적인 메모리 사용을 가능하게 한다.이러한 개념들은 머신 레벨 프로그래밍을 이해하고 하드웨어와 소프트웨어 간의 상호 작용을 깊이 있게 이해하는 데 매우 중요한데 ISA는 하드웨어와 소프트웨어 간의 다리 역할을 하며, 가상 주소는 메모리 관리를 효율적이고 안전하게 만들어 준다. 고급 언어의 추상화는 프로그래머가 하드웨어의 복잡한 세부 사항을 신경 쓰지 않고 효율적으로 코딩할 수 있게 한다.
x86_64의 가상 주소의 경우 64비트 워드로 표현됨.callq 명령어의 인자를 바꿈.return(req) 다음에 nop라는 명령어가 삽입되지만 아무일도 하지 않고 단지 메모리 시스템의 성능을 위한 것이다.부동 : 한자로 떠다니며 정해진 위치가 없음
고정 소수점 : 숫자를 정수 부분과 소수 부분으로 나누어 각각 고정된 위치에 저장
부동 소수점 : 큰 범위의 값을 표현하기 위해서

가수와 지수로 구분하여 표시

부동 소수점, 고정 소수점
부동소수점 수는 두가지의 포맷으로 나뉜다.
또한 x86계열의 프로세서들은 역사적으로 특별한 80비트 (10바이트) all floating-point 명령을 이용했다. (호환되지 않을 수 있음)
%pstp 는 런타임 스택에서 마지막 자리를 가리키는 데 쓰인다. 몇몇 명령어들은 이 레지스터에 I/O를 수행함.하나의 명령어는 보통 “연산자 코드”, “모드”, “피연산자 지시자”로 구성됨
명령어의 피연산자는 아래 세가지 타입으로 분류된다.
mov $5, %eax에서 $5는 즉시 값으로, 레지스터 eax에 5를 저장합니다.$ 기호 뒤에 정수를 붙여 사용합니다. 예: $-577 또는 $0x1F.add %eax, %ebx에서 %eax와 %ebx는 레지스터입니다. 이 명령어는 eax의 값을 ebx에 더합니다.mov 0x10(%ebx), %eax에서 0x10(%ebx)는 메모리 주소를 참조합니다. 이 명령어는 ebx 레지스터의 값에 16을 더한 주소의 값을 eax 레지스터에 저장합니다.$5.%eax.0x10(%ebx).이렇게 각각의 오퍼랜드 타입을 이해하면 어셈블리 언어에서 데이터를 어떻게 처리하는지 쉽게 파악할 수 있다.
Figure 3.3에서 보이는 다양한 형태들은 이러한 주소 계산 방식의 예시입니다. 가장 일반적인 형태는 다음과 같습니다:설명: Imm(rb, ri, s)는 메모리 주소를 계산할 때 사용하는 일반적인 형태입니다.
계산 방식: Imm + R[rb] + R[ri] * s
Imm: 즉시 값 (Immediate value)R[rb]: 레지스터 rb의 값R[ri] * s: 레지스터 ri의 값에 s를 곱한 값용도: 이 형태는 주로 배열의 원소에 접근할 때 사용됩니다.
배열 접근 예시:
- 배열: arr[5]라는 배열이 있다고 가정해봅시다.
- 주소 계산: 배열의 원소에 접근하기 위해 메모리 주소를 계산해야 합니다.
- 계산: Imm + R[rb] + R[ri] * s 형태로 주소를 계산합니다.
- Imm: 배열의 시작 주소 (예: 0x1000)
- R[rb]: 배열의 시작 주소 (예: 0x1000)
- R[ri] * s: 배열의 인덱스와 원소 크기 (예: 5 * 4 바이트 = 20 바이트)
**주소 계산 예시**: 만약 `Imm`가 0x1000이고, `R[rb]`가 0이고, `R[ri]`가 5이며 `s`가 4라면:
- 계산: `0x1000 + (5 * 4) = 0x1000 + 20 = 0x1014`
- 결과: 주소 0x1014에 위치한 데이터에 접근합니다.
그림의 나머지 형태들은 위의 기본 형태에서 일부가 빠진 것이며, 다양한 주소 계산 방식을 제공합니다
Imm(rb, ri, s) 형태는 주소를 계산할 때 가장 일반적으로 사용되는 형태입니다.
주소 계산: Imm (즉시 값) + R[rb] (기본 주소) + R[ri] * s (인덱스와 스케일)
용도: 배열의 원소에 접근하거나, 메모리 주소를 동적으로 계산할 때 사용됩니다.
이해를 돕기 위해 간단한 예시와 설명을 통해 오퍼랜드 형태와 주소 계산 방식을 정리했습니다.
데이터 이동 명령어: 데이터를 메모리와 레지스터 간에 복사하는 데 사용됨, 각 명령어는 이동할 데이터의 크기에 라 다름
데이터 이동 명령의 source operand들은 값들이 메모리 혹은 레지스터에 담긴 Imm이여야 함. Destination operand로는 레지스터나 메모리 주소를 지정함.
movb Move bytemovw Move wordmovl Move double wordmovq Move quad wordmov 는 destination operand가 가리키는 특정한 레지스터 바이트 혹은 메모리 위치만을 업데이트한다.movl(32비트)의 목적지가 레지스터 일때, 레지스터 값의 높은 순위의 4바이트가 0으로 설정된다.movz , movsmovz 계열 명령어들은 남은 바이트를 0으로 채운다. 그러나 movs 명령어들은 sign extension된 값들을 채운다. 그리고 source operand의 MSB(Most Significant Bit)를 복제한다.
long exchange(long *xp, long y) {
long x = *xp;
*xp = y;
return x;
}
exchange:
movq (%rdi), (%rax)
movq %rsi, (%rdi)
ret
최적화된 코드
_exchange:
LFB0:
pushq %rbp
LCFI0:
movq %rsp, %rbp
LCFI1:
movq %rdi, -24(%rbp)
movq %rsi, -32(%rbp)
movq -24(%rbp), %rax
movq (%rax), %rax
movq %rax, -8(%rbp)
movq -24(%rbp), %rax
movq -32(%rbp), %rdx
movq %rdx, (%rax)
movq -8(%rbp), %rax
popq %rbp
LCFI2:
ret
%rdx 가 x라는 값을 가지고 있을 때 leaq 7(%rdx, %rdx,4)를 하면 5x+7이 된다.incq (%rsp)는 스택의 8byte(quad word) top 원소가 증가하게 된다. 이 문법은 C의 ++나 -- 와 같다.x-=y 같은 구문을 연상시킨다. 예를 들어 subq %rax , %rdx 같은 명령은 %rdx를 %rax의 값으로 감소시킨다.Imm이나 단일 바이트 레지스터인 %cl로 정할 수 있다. (이 명령어들은 오직 한 레지스터만을 피연산자로 쓴다는 점에서 이상하다)w비트의 길이를 가진 데이터 값으로 동작하는 쉬프트 명령어는 쉬프트의 양을 레지스터 %cl의 낮은 순서 m비트로부터 결정하다. (2^m =w) 높은 순서의 비트들은 무시된다.%cl 이 0xFF라면 salb 는 7만큼 쉬프트 (w가 8이므로 m 은 3이고 0xFF의 낮은 3비트의 값은 7이기 때문이다)CF (unsigned) t < (unsigned) a Unsigned overflow
ZF (t==0) 0
SF (t<0) Negative
OF (a < 0 == b < 0) && (t < 0 != a < 0) Signed Overflow