본 시리즈은 비전공자가 공부한 computer science, 그중에서도 컴퓨터구조를 정리한 글입니다. 틀린내용이 있다면 댓글로 알려주시면 정말정말 감사하겠습니다. 참고한 책은 Computer Organization and Design MIPS Edition입니다
우리가 어떤 변수의 값을 불러올때 어떤 현상이 발생할까를 한번 생각해보고 들어가면 좋을거같습니다.
그림을 보면 우선 register에 실제 메모리에 있는 값을 load해오고 연산이 끝나면 해당 결과를 다시 메모리에 올리는 일련의 과정을 거치게 됩니다. 그러므로 우리는 메모리에서 값을 가져오고 올리는 연산을 배울 필요가 생기는겁니다
sw
는 말그대로 register에 있는 값을 memory에 저장하는 연산을 의미합니다. 우선 전체적인 그림을 그림으로 정리해봤습니다.
한번 해석을 해볼까요 우리는 4번 register에 있는 값을 메모리에 저장할건데 어디에다 저장할거냐 5번 register의 주소로부터 8칸 떨어져있는 곳에다가 해당 값을 저장할거다 라는 의미입니다
둘다 모양이 비슷하죠 lw의 경우에 5번 register에서 8칸떨어진곳에서 데이터를 가져와서 4번 register에 넣어주겠다는 의미입니다.
lui $5, 0x1000
lui $4, 0x789A
ori $4, $4, 0xBCDE
sw $4, 8($5)
lw $6, 8($5)
첫번째 줄을 보면 lui연산을 통해서 5번 register에 0x10000000이라는 32bit값을 저장했고
두번째줄과 세번째줄을 보면 lui와 ori를 통해 4번 register에 0x789ABCDE라는 32bit값을 저장했습니다
그리고 나서가 중요한데 sw연산을 위해서 필요한건 어디다
인데 5번 register에 저장된 값에서 8칸떨어진곳이 저장할 메모리 address가 된다 즉, 0x10000000에서 8칸 떨어진 곳이 됩니다
왜 저기서부터 789가 시작하는지를 설명해보면 우선 [10000000]이 5번 register의 값을 가진 address이고 1byte당 다음 address를 가집니다. 그렇다면 8칸떨어져있다는뜻은 8byte만큼 떨어져있다는 뜻이고 한칸당 4bit니까 두칸이 하나의 address일거다 그러면 총 16칸 떨어져있는 곳이 값을 저장할 메모리 주소라는 뜻이 되는거겠죠
🔥궁금한거: 바이트당 하나의 address면 4byte로 해당되는 값은 네개의 address를 가지는거고 그러면 78은 8번째 주소에 9a는 9번째주소에 bc는 10번째주소에 de는 11번째 주소에 있는거라고 생각하면되는건가?
저번에 I-format에 대해 배울때 addi ori andi외에 store load operation에서도 해당 포멧이 쓰인다고 했는데 배웠으니 한번 써보면 될거같네요
sw $4, 8($5)
위와 같은 수식을 I-format으로 바꿔보면 우선 sw의 op는 43이고 6bit이니까 101011이고 destination register의 위치가 맨뒤로 가므로 그 위치에 있는 $4인 00100이 rt자리에 들어가고 base register인 $5, 00101이 rs에 들어갑니다 offset인 8은 constant위치에 16bit으로 가고 0000 0000 0000 1000이 들어가서 결론적으로 1010 1100 1010 0100 0000 0000 0000 1000
이 되서 0xaca40008
이라는 binary값이 됩니다
disassemble하는 예시도 하나 들어보면0x8ca60008
이라는 값은 1000 1100 1010 0110 0000 0000 0000 1000
이고 6 5 5 16으로 쪼개면 결론적으로는
100011 / 00101 / 00110 / 0000 0000 0000 1000
아래와같은 I-format으로 표현이 가능하고 op를 map에서 찾으면 lw인걸 알 수 있고
rt가 6이고 rs인 base register가 5이고 offset이 8이니까
lw $6, 8($5)
라는 assembly text로 변환이 가능합니다
그림을 보고 메모리에 실제로 올라가는 flow를 한번 해석하고 가는게 좋을거같아서 정리를 해보겠습니다.
해당 메모리주소에 값이 잘 올라간것을 확인할 수있습니다(두칸당 한byte니까 8칸에 데이터하나씩 잘 들어가있네요)
자그러면 아래에있는 main을 해석해봅시다
그러면 어떤일이 일어날까 결과를 보기전에 생각을 해보면 10020008에 789a0000이 저장되겠네요 현재는 10020008에 0xabcde12가 들어가있으니까 해당 값대신 789a0000이 값이 들어갈겁니다.
실제 결과를 봐도 예상한 결과가 그대로 실행된것을 알수있습니다
사실 이건 간단한 개념이니까 쓱 훑고 넘어가면 우리가 "가나다라"라는 글자를 저장할때 "라","다","나","가"의 순서대로 저장하는걸 Little endian방식이라고하고 "가","나","다","라"의 순서대로 저장하는걸 Big endian이라고 합니다. 방금예제같은 경우도 실제로는 Little endian으로 저장되기때문에 우리가 읽을때는 끝에서부터 순서대로 읽으면 됩니다
위의 사진처럼 1byte에 하나의 숫자를 넣었다고 해봅시다. .byte 's'를 실행했을때 다음 저장할 메모리주소는 10010006이지만 .word 1의 명령을 수행하니 1이라는 값이 10010008에서 저장이 된걸 알 수 있습니다. 이는 데이터가 4byte면 메모리주소가 4의배우에서 저장되어야하는 규칙이 있기때문이고 이를 word alignment라고 합니다
마지막으로 메모리연산관련 예제를 보고 다음 섹션으로 넘어가려고 합니다
A[2] = h + A[0]
이라는 코드가 있다고 가정하고 h라는 값은 $s2에 A의 base address는 $s3에 있다고 가정해보겠습니다.
그러면 우선 A[0]은 base address에서 0의 offset메모리에서 값을 저장해야합니다($t0에 저장한다고 가정)
그리고나서 $t0에 저장된 값과 $s2에 저장된 값을 더해서 $t0에 임시로 저장한뒤에 해당 값을 base address에서부터 8의 offset의 메모리에 저장해야합니다(int가 2번 떨어진거니까 4byte * 2 = 8)
assembly text로 정리해보면
lw $t0, 0($s3)
add $t0, $s2, $t0
sw $t0, 8($s3)
가 될거고 메모리를 그림으로 표현해보면
이와같이 표현할 수 있습니다. 설명을 붙여보면 $t0의 값이 2가되었다가 5가되고 0x00000018에는 값이 없다가 5라는 값이 저장되는 순서로 작동하게됩니다.
분기 명령어의 세가지 operator를 알아보면 가장 먼저 jr이 있습니다
jr $31
위와 같이 사용하며 jump register이라는 뜻을 가지고 있는데 register가 가지고 있는 값과 동일한 메모리주소에 jump하라는 명령입니다
우선 jr을 보면
main:
nop
la $t1, main #main을 t1에 load하겠다는 뜻
jr $t1
실행하면 우선 main의 address가 0x00400024로 정해지게되고
nop을 건너서 $t1에 32bit 값을 넣어줘야하기때문에 lui와 ori연산을 통해 main의 메모리시작주소가 $t1에 저장되게 됩니다. 그리고 jr $t1을 실행해주면 0x00400024메모리주소로 jump하라고 하고 실제로 spim에서보면 nop을 실행하는 메모리주소가 0x00400024인걸 확인 할 수 있습니다
jr명령어를 이진수로 바꾸기 위해서는 opcode map을 보면 funct field에 jr이 있기때문에 R-format을 사용한다고 생각하면됩니다. 다만 조금 헷갈리는 부분인 jr의 operand는 rd인가를 생각해보면 한국어의 해석상 목적지가 맞지만 여기서 목적지는 데이터를 저장할 register이기때문에 rs에 operand를 저장해야합니다
rt,rd,shift constant가 전부 0이기때문에 op와 funct rs만 채워주면됩니다
j의 경우에
j L1
위와 같이 사용하는데 L1의 뜻은 label로 label을 사용하면 complier가 자동으로 메모리주소를 부여하기때문에 실제로 L1 == 이동할 메모리주소
라고 생각하면 편합니다
main:
nop
j main
조금전에 봤던 jr과 비슷한데 주소값을 저장할 register를 설정하지 않아도 되기때문에 간결한 코드가 된거같습니다. 실제 동작은 jr과 동일합니다
j명령어를 이진수로 바꾸기 위해서는 J-format이라는 형식에 대해 알아야합니다. 하지만 훨씬간단합니다
op field와 BTA 즉, jump할 메모리주소를 넣어주면 됩니다. 하지만 여기서 우리는 instruction을 이진수로 바꾸는것보다는 해당 binary에서 BTA를 구하는 방식에 익숙해져야합니다
즉, spim에서 확인해보면 해당 명령어가 0x08100009라는 16진수로 실행되었는데 이를 이진수로 바꾸면
0000 1000 0001 0000 0000 0000 0000 1001
이고 앞에 6bit가 op이므로
00 0001 0000 0000 0000 0000 1001
이라는 값을 가지고 BTA를 구해야합니다. 방법은 간단합니다. 맨뒤에 00
을 붙여주고(이유는 명령어주소가 4의 배수이기 때문에) 맨 앞에는 해당 명령을 실행한 pc주소입니다. 해당명령어를 00400028이라는 주소에서 실행했으므로 맨앞에 0을 이진수로바꾼 0000을 넣어서
0000 00 0001 0000 0000 0000 0000 100100
이고 이글 16진수로 바꾸면 0x00400028이 되고 이는 BTA즉 jump할때 갈 주소인 nop의 주소가됩니다
jal의 경우엔 jump and link라는 뜻을 가지고 있고
jal L1
j와 비슷한거같지만 큰 차이점은 해당 명령은 추가로 다음 메모리주소를 $ra(return address)에 저장해줍니다
실제 코드를 보면서 해석해보면
main: addi $a1, $0, 1
jal foo
lui $t0, 0x1001 #pc주소 0x0040002C
sw $a1, 0($t0)
foo: add $a0, $a1, $a1
jr $ra
차근차근 순서대로 해석을 해보면
jal도 j-Format을 채택해야하므로 실제 이진화된 instructon 코드에서부터 BTA를 알아내는 것이 중요합니다.
0x0c10000d
라는 이진화된 instruction을 가지고 있고
0000 1100 0001 0000 0000 0000 0000 1101
로 표현할 수 있고 앞에 6bit를 끊어서 보면
00 0001 0000 0000 0000 0000 1101
이라는 결과가 나오고
맨뒤에 00을 붙이고 해당 instruction은 pc 주소가 00400028이므로 맨 앞 0을 0000이라고 할 수 있으므로
0000 00 0001 0000 0000 0000 0000 1101 00
이 된다. 이 값은 16진수로 0x00400034이고 spim해서 확인하면 다음 실행주소인 foo의 주소와 일치하는 것을 알 수 있습니다
branch equal이라는 뜻의 operator이고 실제 사용방식은 beq $rs, $rt, L1
인데 rs와 rt의 register에 들어있는 값이 같다면 L1의 주소의 instruction을 수행하고 아니면 다음 명령어를 수행하라는 연산입니다
branch not equal이라는 뜻이고 실제 사용방식은 beq와 같은데 rs와 rt의 값이 같지 않으면 L1의 주소로 jump해서 instruction을 수행하고 아니면(값이 같다면) 다음 명령어를 수행하라는 연산입니다
main: bne $s3, $s4, Else
add $s0, $s1, $s2 # f=g+h
j Exit
Else: sub $s0, $s1, $s2 # f=g-h
Exit: add $s0, $s0, $s0 # f=2*f
위와같은 코드가 있을때 만약 s3와 s4의 값이 같지 않다면 else로 가서 sub를 실행하고 만약에 같다면 add명령어를 실행하고 끝나면 j를 실행해서 exit으로 가서 add를하게됩니다.
beq와 bne의 경우에 op map을 확인해보면 I-format인걸 확인할수있습니다. 그래서 이 format도 결국은 jump와 비슷하게 BTA를 알아내는 방식에 익숙해져야합니다.
해당코드의 이진화된 instructon은 16740003이고 이를 I-format대로 나눠보면
000101 / 10011 / 10100 / 0000 0000 0000 0011
이라고 할 수 있습니다.
그리고 해당 pc address값과 bne의 constant에 sign extenstion을 해주고(왜? 실제로는 offset이들어가기때문에 sign extension) 맨뒤에 00을 넣어준 값을 + 연산해주면 BTA를 구할 수 있게됩니다.
실제로 BTA의 값을 비교해봐도
동일한 값을 얻을수있게됩니다.
이때 주의할점은 bne와 beq의 L1이 너무 멀면(메모리 기준으로) sign extension시 올바른 값을 설정할수없기때문에 offset값이 크지 않아야합니다.
L1이 너무 먼경우엔 bne를 beq로, beq를 bne로 바꿔서 true시 가까운 L1으로 실행문을 만들어서 compiler가 새로 coding을 하게됩니다.