본 시리즈은 비전공자가 공부한 computer science, 그중에서도 컴퓨터구조를 정리한 글입니다. 틀린내용이 있다면 댓글로 알려주시면 정말정말 감사하겠습니다. 참고한 책은
Computer Organization and Design MIPS Edition
입니다
representation: 표현
우리가 흔히 쓰는 코드들 예를들어서 아래와 같은 것을을 High-level language program
이라고 합니다
func add(a: int, b: int) -> int {
return a+b
}
그렇다면 이러한 코드를 컴퓨터가 읽을수 있을까요? 라고 물어보면 아마 대부분그렇지 않다는사실은 알고있을겁니다.
저도 비전공자지만 저 코드를 컴퓨터가 읽는 언어가 아니라는건 알고 있더라고요
그러면 어떤 과정을 거쳐서 저 코드를 컴퓨터가 이해하고 수행하게 될까?
가 이번 주제의 핵심입니다.
이 부분을 알기전에 우리가 컴퓨터를 배우게되면 꼭 배우는것중에 컴퓨터는 0과 1만 읽을 수있다라는 정도는 아마도 배웠을거예요. 그 지식대로 결국 컴퓨터는 0과 1로 이루어진 명령(instruction)들만 읽을 수 있다는 말은 어떤 방식인지는 모르겠지만 우리의 코드를 0과 1로 바꾸는 과정이 존재한다는 뜻이 됩니다. 사실 저도 이정도까지만 알고 있었는데 좀더 자세한 내용을 살펴보도록 하겠습니다.
우리가 만든 코드 즉, High level language를 0과 1의 2진수 instruction으로 바꾸는걸 compliler
라는 녀석이 맡아서 해주게 됩니다. 그러면 바로 0과 1로 바꿔주냐? 그건 또 아닙니다. 그 중간 과정에서 Assembly language라는 걸로 바꿔주고 0과 1의 intstruction으로 바꿔줍니다. Assembly language를 0과 1로 바꿔주는 친구는 Assembler
라고 합니다.
정리를 해보면
결론적으로 우리가쓰는 코드를 0과 1의 명령들로 바꿔주는 친구는 complier
라고 하는데 이 compiler가 코드를 바로 0과 1로 바꿔주는게 아니고 assembler
라는 친구를 이용해서 코드 -> 어셈블리언어 -> 0과1
의 과정을 거쳐 최종적으로 0과 1의 명령어로 변환을 해주게 됩니다.
책에 자주나오는 그림인데 약간의 해석을 붙여보면 complier가 코드(동그란친구)를
0과1의 명령(정육면체)로 바꿔주고 그 명령들을 메모리에 저장하면 명령들을 input한후 control이 해석해서 명령을 실행(output)해준다
🔥(8/8스터디) - 여기서 processor는 CPU를 뜻함. 운체에서의 process와 다름
sequence: 앞뒤가 꼭 지켜져야하는 시공간적순서
(알고리즘의 세세한 부분을 완성시켜준다)
직역을 해보면 명령집합구조?라고 할 수 있는데 실제 뜻도 이와 비슷합니다. 컴퓨터에서 사용되는 명령어들의 집합 및 그 정의
라고 합니다. 좀더 공부를 해봐야알겠지만 컴퓨터에서는당연히 여러개의 명령들이 있을텐데 그 명령들을 정리해놓은 구조가 아닐까 싶습니다. 현대컴퓨터의 대부분에서는 간단한 명령어들의 집합을 가지고 있다고 하네요.
그리고 이 책에서는 여러개의 명령어구조(ISA)중에서 MIPS ISA를 배운다고 합니다
구글링을 해보니 이런 표가 나오더라고요? ISA라는건 이런식으로 더하고 빼고 하는 여러가지 명령(비슷한의미로 연산?)을 정해놓고 그 명령어를 사용해서 명령을 수행할수있게 해주는거같습니다. 이 책에서는 분명 여러가지 기준으로 명령어들을 묶어놨을텐데 MIPS라는 이름의 명령어들의 체계를 공부한다고 생각하면될거같습니다
그렇다면 본격적으로 MIPS ISA를 배우기전에 Types of Instructions 명령어들의 타입들에 대해서 간단하게 알아보도록 하죠. 하나하나 앞으로 배울거기때문에 우선 이런게있다 정도로 넘어가겠습니다
첫번째 타입은 Arithmetic / Logic instructions 연산명령어
입니다. ALU operation이라고도 부르는거같습니다. 뭔지는 아직 모르겠지만 연산이라고하니 더하기 빼기 곱하기 나누기 이런게 아닐까 싶네요
operation: 컴퓨터 중에서 행해지는 「연산」, 기기류의 「조작」, 「움직임 자체」 등을 가리키는 경우가 많다.
두번째 타입은 Data transfer instructions
라는 메모리접근 명령어입니다. Load/Store instruions와 같은말이라고도 하네요. 이것도 단어자체에서 유추해보자면 읽거나 쓰거나 저장하고나 불러오는 이런게 아닐까 싶네요
마지막 세번째 타입은 Branch instructions
라는 분기명령어입니다. Control transfer instructions라고 합니다. 이건진짜 대체 뭐하는 명령어인지 모르겠네요 git에서 쓰는 branch같은걸까요...?
🔥(8/8스터디) - if문 while문같은 분기처리 및 for문같은 loop를 뜻함
Arithmetic Instruction은 제가 예상했던대로 덧셈, 뺼셈, 곱셈, 나눗셈 등의 산술논리 연산 명령어들
이라고 합니다. 실제로 구성이 어떻게 되어있는지를 보면
우선 하나의 instruction은 operation과 두가지의 operand라는 걸로 이루어져있습니다. 그리고 개발할때 빼놓을수없는 comment(주석)도 있네요 필수는 아닌거같습니다. 여기서의 주석은 #
으로 표현하네요(python이랑 동일)
operand: 피연산자
MIPS ISA에서 하나의 명령은 하나의 연산자와 두종류의 피연산자로 이루어져있습니다. 피연산자의 종류에는 destination operand와 source operand가 있는데 사실 말그대로 입니다. destination은 연산의 결과가 복사되어 들어갈 operand이고 source는 연산의 주체가되는 operand를 뜻합니다. 위의 그림은 한마디로 요약하면 source값들의 operation결과를 destination에 넣어주는 연산
입니다
즉, 우리가 code로 아래와 같이 적으면
f = g + h
complier가 이를 assemble language로 변환해주는데 그 결과가 아래가 되는겁니다
add f, g, h #f=g+h
결과적으로 보면 하나의 명령에는 3개의 operand가 들어가는데 하나는 destination operand 두개는 source operand인거죠.
위의 예시들처럼 operand에는 변수명을 사용하면 안됩니다.
차근차근 설명을 해볼게요. 왜일까요? 변수라는건 우리가 code로 작성했을때 필요한 source라고 할 수있습니다. source로 대입해서 말하는게 정확한지는 모르겠네요 그냥 code를 작성할때 필요한 요소라고 생각해도 좋습니다 즉 High level language에서 쓰이는건데 이는 Assemble language에서는 사용할 수 없다고 생각하면 좋을거같습니다.
그러면 대체 변수명이 아니라 뭘써야하나요?
변수명 대신 register(레지스터)의 이름을 사용해야합니다. 그러면 레지스터가 뭔지 그 특성들에 따라 어떻게 사용되는지를 묶어서 이해하면 이해하기시 조금 편할거같아요.
레지스터: 프로세서 내부에 있는, 작고 빠른 임시의 메모리
우선 레지스터는 1개가 32bit이고(1비트가 0과 1을 표현할수있다고 했으니 2진수로 표현하면 32개가 쫙 나열된 형태겠네요) MIPS에서는 총 register가 32개존재합니다. 이름은 앞에 $과 레지스터 번호이기때문에 $0부터 $31까지의 레지스터이름을 가지게됩니다. 그리고 32bit는 word
라는 단위로 불리게됩니다.
🔥(8/8스터디) - 32bit = word 는 무조건적인 단위는 아님
저희가 주목해야할건 작다
, 빠르다
, 임시
라는 세가지 키워드입니다.
이러한 특징때문에 레지스터는 자주접근하는 데이터를 위해 사용되고(빠르다) 메모리에 저장될필요 없이 프로세서내부에 존재하게됩니다(작다, 임시)
반대로 용량이크게되면 CPU(프로세서)에 존재할수없어 메모리에 저장되어야하고 당연히 읽고쓰는데 속도가 느려진다는 단점이 발생하겠지만 용량이크다는 확실한 장점을 가지게 됩니다
자, 그러면 본격적으로 레지스터의 이름을 가지고 Complied MIPS code를 확인해보겠습니다
위의 설명을 종합해보면 아래와 같은 간단한 연산 코드는
f = g + h
아래와 같이 complier가 assemble language로 변환해줍니다
add $3, $4, $5
해석을 해보면 $4라는 register에 있는 값이랑 $5라는 register에 있는 값을 add라는 이름의 operation을 수행하고 그 값을 destination operand인 $3에 임시저장해라라는 명령어가 됩니다.
여기서 끝내도 괜찮겠지만 아주 조금더 복잡한 코드를 바꿔봅시다
이번엔 아래의 코드를 한번 바꿔볼까요 참고로 뺄셈을 수행하는 operation은 sub입니다
f = (g+h) - (i+j)
f라는 값은 $2에 g는 $3에 h는 $4에 i는 $5에 j는 $6에 저장한다고 가정해보면
아래와같은 assemble language로 complier가 변환해줄겁니다
add $7, $3, $4 # $7 = g + h
add $8, $5, $6 # $8 = i + j
sub $2, $7, $8 # f = $7 - $8
좀전에 배웠던것처럼 하나의 instruction에는 세개의 operand가 들어가는데 하나는 destination 두개는 source였죠. 그런데 우리가 변수를 사용하지 못하는 이유도 충분히 이해를 했는데 꼭 굳이 register이름을 operand에 넣어줘야할까?라는 생각이 드는 동시에
그냥 값을 operand에 넣어줄 순 없을까?
라는 생각이 들었습니다.
그리고 우리는 한가지 룰만 지킨다면 굳이 register이름이 아니라 값을 직접 넣어줄 수있습니다. 한가지 룰은 바로 세번째 operand인 soucer operand의 두번째 operand에만 한정해서 값을 바로 넣어줄수 있다는겁니다. 참고로 이상황에서 operaion은 add
가 아니라 immediate를 넣은 addi
를 사용해야합니다
즉, 아래와 같은 코드를
f = g + 10
아래와같이 바꿀수 있다는 말이됩니다
addi $2, $3, 10 # 3번째 operand만 상수가 가능
큰 주제로 넣어야할까말까 고민을 했는데 짧지만 꽤나 중요한 내용이라고 생각이되어서 넣어봤습니다. 0번째 register의 값은 항상 0이라는 규칙입니다
$zero == $0 == 0
위의 세가지는 0이라는 값으로 동일합니다. 당연히 0이라는 값으로 그대로 쓰고싶다면 3번째 operand에서만 사용이가능하겠죠(addi와 함께)
이를 통해서 한가지 유용한 일반적인 operation를 사용할 수 있는데 코드를 먼저보면
add $5, $4, $0
위의 코드의 뜻은 $4레지스터의 값을 그대로 $5에 복사해서 넣으라는 값을 move할수있는 operation이 됩니다. addi를 사용하면 아래와 같겠네요
addi $5, $4, 0
addi $8, $0, 10
이 명령을 수행한 후에 8번레지스터의 값은 얼마인지 10진수와 16진수로 쓰시오
10의 값을 그대로 8번레지스터에 move하는 operation이니까 8번 레지스터에는 10이라는 값이 저장되게 됩니다
10진수로는 10이고
16진수로는 음.... 보통 2진수로바꾸고 16진수로 바꾸니까 한번해보죠
10이라는 숫자를 2진수로 바꿔보겠습니다 하지만 그전에 레지스터하나는 32비트라고 했으니 2진수를 표현할수있는 1비트가 32개가 있습니다
0000 0000 0000 0000 0000 0000 0000 0000
2와 8을 더하면 10이 되므로
0000 0000 0000 0000 0000 0000 0000 0101
이 되고 각 4자리를 끊어서(2의 4제곱은 16이므로)
0x0000000A
라고 표현할 수 있습니다.
MIP instruction표를 보면 add, addi, sub가 있는데 subi가 없습니다
왤까를 곰곰히 생각해보면 제가 처음에 이런이야기를 했었습니다
현대컴퓨터의 대부분에서는 간단한 명령어들의 집합을 가지고 있다고 하네요.
이 말은 연산자체가 다른거같아도 기존에있는 연산으로 처리가 가능하다면 굳이 새로운 operation을 만들어서 사용할 필요가 없게됩니다.
subi가 필요하다는건 우리가 세번째 operand를 상수로 사용하고 해당값을 빼는 operation을 수행하고 싶다는 뜻인데 사실 이건 addi로도 수행이가능합니다,
addi $5, $6, -5
이런식으로요. 그러면 우리가 배워하는 한가지 개념이 더 생겼습니다. 우리가 지금까지는 정수중에서도 양수만가지고 이진수로바꿔서 컴퓨터에게 명령을 내렸는데 우리가 이진수로 음수를 표현하는 방법을 배워야 subi역할을하는 addi명령을 수행할 수 있게됩니다.
사실 2진수로 음수를 표현하는 방법이 세가지정도있다고 하는데 그중에서도 2의 보수방식으로 음수를 표현하는 방법에 대해 배워보겠습니다.
음수가 없다고 가정하면 하나의 register는 어떤수까지 표현이 가능할까요?
0000 0000 0000 0000 0000 0000 0000 0000 0000의 0부터
1111 1111 1111 1111 1111 1111 1111 1111 1111의 4294967295까지
우리는 이를 unsigned 32bit 2진수
라고 합니다
그러면 음수가 있다면
0000 0000 0000 0000 0000 0000 0000 0000 0000의 0부터
0111 1111 1111 1111 1111 1111 1111 1111 1111의 2147483647까지
1000 0000 0000 0000 0000 0000 0000 0000 0000의 -2147483647부터
1111 1111 1111 1111 1111 1111 1111 1111 1111의 -1까지
윌는 이를 signed 32bit 2진수
라고 합니다
만약에 그냥 unsigned라면 맨앞에숫자(32자리수)는 2^31
를 의미하지만 signed라면 -2^31
을 의미합니다
이게 정말 정확한 개념이고 우리는 매번 이 32자리를 곱해가며 더할수없기때문에 보환된 표현법에 대해서 배우면 큰 도움이 됩니다.
우선 가장 기본적인 순서는 아래와같습니다.
음수라면? 반전시키고 1을 더한다
예를 들어서 우리
1111 1111 1111 1111 1111 1111 1110 1001
이라는 이진수가있다고 가정해봅시다. signed라고하면 맨 앞자리가 1이기때문에 음수라는걸 알수있습니다.
🔥(8/8스터디) - signed와 unsigned를 알수있는 operation이 존재하고unsigned인경우 맨앞자리가 0일때 생략함(프로그램상에서)
음수라면 반전시키는게 우선이니 해당 이진수를 반전시켜보면
0000 0000 0000 0000 0000 0000 0001 0110
이고 이번인 1을 더해봅시다 그러면
0000 0000 0000 0000 0000 0000 0001 0111
이런 결과가 나오고 10진수로 바꿔보면 1+2+4+16이니까 23이나오고 이는 절댓값이니까 -23이라는 값이 나오게됩니다.
그럼 반대로우리가 -23이라는 10진수를 2진수로 바꾸고싶을땐 어떻게 해야할까요?
우선 23을 2진수로 표현합니다
0000 0000 0000 0000 0000 0000 0001 0111
그러면 여기서 음수니까 반전시킵니다
1111 1111 1111 1111 1111 1111 1110 1000
그리고 1을 더해줍니다
1111 1111 1111 1111 1111 1111 1110 1001
이진수를 -23으로 바꾸거나 다시 이진수로 바꾸는과정이 동일하고 그과정에서 반전과 1을더하는 과정이 필요하다는걸 알 수있습니다