2.11 Parallelism and Instructions:Synchronization
병렬 처리는 독립적인 작업을 수행하는 것이다.
하지만 작업들이 협업해야 할 때, 동기화를 필요로 한다.
RISC-V에서는
lr.w(load-reserved word)
sc.w(store-conditional word) 를 사용한다.
2.12 Translating and Starting a Program
- A translation hierarchy for C
1. compiler
고수준 언어로 작성된 C프로그램을 어셈블리 언어 프로그램으로 변환한다.
2. assembler
어셈블리 언어 프로그램을 기계 코드로 변환하여 오브젝트 파일을 생성한다.
3. linker
여러 오브젝트 파일을 결합하여 실행 가능한 파일을 생성한다.
4. loader
실행 파일을 메모리에 로드하고, 실행을 시작한다.
//Dynamically Linked Libraries
프로그램이 실행되는 동안 라이브러리를 동적으로 연결하여 사용한다.
-Question
Which of the advantages of an interpreter over a translator was the most important
for the designers of Java?
Java는 다양한 플랫폼에서 동일하게 실행되도록 설계되었다. 이를 위해 Java는 소스 코드를 기계 코드로 번역하는 대신 바이트코드로 컴파일하고, 이 바이트코드를 JVM(Java Virtual Machine)이 해석하여 실행한다. 이렇게 하면 Java 프로그램은 플랫폼에 관계없이 동일한 바이트코드를 실행할 수 있으며, 결과적으로 “한 번 작성, 어디서나 실행(Write Once, Run Anywhere)“이라는 철학을 구현할 수 있게 된다.
2.13 A C Sort Example to Put it All Together
swap 함수가 c코드에서 어셈블리로 바뀌는 과정.
//c code
void swap(int v[], size_t k) {
int temp;
temp = v[k];
v[k] = v[k+1];
v[k+1] = temp;
}
//assembly code
swap:
slli x6, x11, 2 // x6 = k * 4 (인덱스를 4배로 해서 바이트 주소로 변환)
add x6, x10, x6 // x6 = v + (k * 4) (배열의 시작 주소에 인덱스를 더함)
lw x5, 0(x6) // x5 = v[k] (v[k] 값을 로드하여 temp에 저장)
lw x7, 4(x6) // x7 = v[k+1] (v[k+1] 값을 로드)
sw x7, 0(x6) // v[k] = x7 (v[k]에 v[k+1] 값을 저장)
sw x5, 4(x6) // v[k+1] = x5 (v[k+1]에 temp 값을 저장)
jalr x0, 0(x1) // 호출한 곳으로 리턴
sort함수가 c코드에서 어셈블리로 바뀌는 과정
//c code
void sort(int v[], size_t n) {
for (i = 0; i < n; i++) {
for (j = i - 1; j >= 0 && v[j] > v[j+1]; j--) {
swap(v, j);
}
}
}
//assembly code
sort:
addi sp, sp, -20 // 스택에 공간 확보 (5개의 레지스터 저장을 위해)
sw x1, 16(sp) // 리턴 주소 저장
sw x22, 12(sp) // x22 저장 (for 루프에 사용)
sw x21, 8(sp) // x21 저장
sw x20, 4(sp) // x20 저장
sw x19, 0(sp) // x19 저장
addi x21, x10, 0 // 매개변수 v를 x21에 복사
addi x22, x11, 0 // 매개변수 n을 x22에 복사
addi x19, x0, 0 // i = 0 초기화
for1tst:
bge x19, x22, exit1 // i >= n이면 exit1로 분기
addi x20, x19, -1 // j = i - 1 초기화
for2tst:
blt x20, x0, exit2 // j < 0이면 exit2로 분기
slli x5, x20, 2 // j * 4 (인덱스를 바이트 주소로 변환)
add x5, x21, x5 // x5 = v + (j * 4)
lw x6, 0(x5) // x6 = v[j]
lw x7, 4(x5) // x7 = v[j+1]
ble x6, x7, exit2 // v[j] <= v[j+1]이면 exit2로 분기
// swap 호출
addi x10, x21, 0 // 첫 번째 매개변수 v 설정
addi x11, x20, 0 // 두 번째 매개변수 j 설정
jal x1, swap // swap(v, j) 호출
addi x20, x20, -1 // j -= 1
jal x0, for2tst // 내부 루프로 분기
exit2:
addi x19, x19, 1 // i += 1
jal x0, for1tst // 외부 루프로 분기
exit1:
// 레지스터 복원
lw x19, 0(sp) // x19 복원
lw x20, 4(sp) // x20 복원
lw x21, 8(sp) // x21 복원
lw x22, 12(sp) // x22 복원
lw x1, 16(sp) // 리턴 주소 복원
addi sp, sp, 20 // 스택 포인터 복원
jalr x0, 0(x1) // 호출한 곳으로 리턴
2.14 Arrays versus Pointers
- 배열을 사용하는 경우
//c code
void clear1(int array[], size_t size){
size_t i;
for (i = 0; i < size; i += 1)
array[i] = 0;
}
//assembly code
addi x5, x0, 0 # i = 0 (register x5 = 0)
loop1:
slli x6, x5, 2 # x6 = i * 4
add x7, x10, x6 # x7 = address of array[i]
sw x0, 0(x7) # array[i] = 0
addi x5, x5, 1 # i = i + 1
blt x5, x11, loop1 # if (i < size) go to loop1
-배열을 사용하지 않는 경우
//c code
void clear2(int *array, size_t size){
int *p;
for (p = &array[0]; p < &array[size]; p = p + 1)
*p = 0;
}
//assembly code
addi x5, x10, 0 # p = address of array[0]
loop2:
sw x0, 0(x5) # Memory[p] = 0
addi x5, x5, 4 # p = p + 4
slli x6, x11, 2 # x6 = size * 4
add x7, x10, x6 # x7 = address of array[size]
bltu x5, x7, loop2 # if (p < &array[size]) go to loop2
//최적화된 asembly code
addi x5, x10, 0 # p = address of array[0]
slli x6, x11, 2 # x6 = size * 4
add x7, x10, x6 # x7 = address of array[size]
loop2:
sw x0, 0(x5) # Memory[p] = 0
addi x5, x5, 4 # p = p + 4
bltu x5, x7, loop2 # if (p < &array[size]) go to loop2
포인터를 사용하는 것이 인덱스를 사용하는 것보다 더 효율적이다.
인덱스는 반목문 내에서 주소 계산이 계속 발생하고
포인터를 사용하면 단순히 포인터를 증가시켜 다음 요소를 가리키게 할 수 있으므로
포인터가 더 효율적이라고 볼 수 있다.
하지만 현대 컴파일러에서는 인덱스를 사용한 것도 포인터를 사용한 것 만큼 효율적으로 최적화할 수 있으므로
현대 프로그래머들은 명시적으로 포인터를 사용하지 않아도 괜찮다.
2.23 Concluding Remarks
• Simplicity favors regularity (단순성은 규칙성을 선호함)
모든 명령어의 크기를 일정하게 유지하고, 연산 명령어의 피연산자를 레지스터로 사용하며, 동일한 위치에 레지스터 필드를 유지함으로써 규칙성을 유지한다.
• Smaller is faster (작을수록 빠름)
더 적은 수의 레지스터가 더 많은 레지스터를 사용하는 것보다 빠르기 때문에 RISC-V는 32개의 레지스터를 사용한다.
• Good design demands good compromises (좋은 설계는 좋은 타협을 요구함)
RISC-V는 큰 주소와 상수를 처리하기 위해 명령어의 길이를 동일하게 유지하면서 큰 상수를 인코딩할 수 있는 타협점을 선택했다.
- RISC-V 명령어 셋
- RISC-V 명령어 빈도
2.25 Self-Study
다음과 같은 2진수가 있을 때 질문에 답하여라.
0000 0001 0100 1011 0010 1000 0010 0011 (two)
hexadecimal format -> 0x14B2823
Decimal format -> 21702691
Does the value change if it is considered a signed number?
최상위 비트가 0이므로, signed, unsigned 모두 부호가 같다.
assembly code
lw x8,20(x22)
풀이
다음과 같은 c언어가 있다.
#include <string.h>
void copyinput (char *input){
char copy[10];
strcpy(copy, input); // no bounds checking in strcpy
}
int main (int argc, char **argv){
copyinput(argv[1]);
return 0;
}
10글자보다 더 긴 입력을 하면 어떤 문제가 발생하는가?
배열의 크기를 넘어서는 입력을 받았기 때문에 오버플로우가 발생한다.
버퍼 오버플로우는 프로그램의 메모리 공간에 예기치 않은 영향을 줄 수 있으며, 특히 공격자가 이 취약점을 이용해 임의의 코드를 삽입하고 실행할 수 있다.
이는 메모리의 반환 주소 등을 덮어쓰는 방식으로 이루어지며, 공격자가 원하는 코드를 실행하여 프로그램의 제어를 탈취할 수 있게 된다.
이를 통해 공격자는 시스템의 권한을 획득하거나 중요한 데이터를 훔칠 수 있는 보안상의 위협을 야기할 수 있다.
//최적화 전
Loop:
slli x10, x22, 2 // Temp reg x10 = i * 4
add x10, x10, x25 // x10 = address of save[i]
lw x9, 0(x10) // Temp reg x9 = save[i]
bne x9, x24, Exit // go to Exit if save[i] != k
addi x22, x22, 1 // i = i + 1
beq x0, x0, Loop // go to Loop
Exit:
//최적화 후
slli x10, x22, 2 // Temp reg x10 = i * 4
add x10, x10, x25 // x10 = address of save[i]
lw x9, 0(x10) // Temp reg x9 = save[i]
bne x9, x24, Exit // go to Exit if save[i] != k
Loop:
addi x22, x22, 1 // i = i + 1
slli x10, x22, 2 // Temp reg x10 = i * 4
add x10, x10, x25 // x10 = address of save[i]
lw x9, 0(x10) // Temp reg x9 = save[i]
beq x9, x24, Loop // go to Loop if save[i] == k
Exit:
'Book > COMPUTER ORGANIZATION AND DESIGN RISC-V' 카테고리의 다른 글
4. The Processor (4.6. An Overview of Pipelining) (0) | 2024.11.15 |
---|---|
4. The Processor(4.1~4.5) (1) | 2024.10.10 |
2. Instructions:Language of the Computer(2.1~2.10) (4) | 2024.10.06 |
Operations (1) | 2024.10.05 |
Computer Organization And Design RISC-V Chapter 1. Exercise (0) | 2024.09.20 |