SP(stack pointer) 레지스터
-스택에 데이터를 쌓거나 반환하기위해서는 내가 현재 어디까지 썼는가,즉 얼마나 스택을 쌓았는가를 알기위한 데이터가필요하다.
sp레지스터는 다음 스택을 쌓아야하는 위치주소를 가지고있다.



fp(frame pointer) 레지스터
-함수가 끝난뒤 반환될때 어디로 반환되어야 하는지 되돌아갈 위치를 알아야한다.
fp에는 현재함수가끝난뒤에 반환될 위치주소가 저장되어있다.

위의 그림에서 보면 fct2 반환후 sp의 위치가  fct2 호출전으로 바뀌어 덮어씌우게(fct2의 지역변수가 먹었던 자원(메모리)을 사용할수있게) 만들어야한다.
하지만 되돌아갈 위치주소를 모르기때문에 fp에 반환될 위치주소가 저장되어 있는것이다.


!!! 이것만 가지고될까?

문제는 함수안에서 또함수를 부를때 발생한다.
fct1에 들어가서 fp에는 8이라는 반환될주소가 저장되있고 sp는 스택을 쌓아올리고있다.
이때 함수 fct2가불린다면,
fct2의 돌아갈위치 16을 fp에 저장하고(8이라는 정보가 날라감!!) sp는 스택을또 쌓아올릴 것이다.

이제 함수가 반환되는 과정을 보자.
fp에 저장된 반환주소로 sp를 옮긴다.
이제 fct1을 반환시켜야하는데 8이라는 값은 아까 덮여쓰여져 없어져버렸다.

우리는 함수호출이 될때 되돌아갈 주소를 fp에 저장해두었는데 이게 한개만 저장했더니 안되는것이다.

문제해결

스택을 이용한다.


 
--함수 호출될때
-sp가 8일때 함수가 호출되면 그스택(메모리)에 fp값 0을 저장해둔다. 
저장하는것은 1스택을 더하기때문에 sp는 12가된다. 
fp에는 sp - 4 (fp가 32bit라고가정)값을 저장한다.

PUSH fp
ADD fp, sp, -4

PUSH명령어 ? :스택에 피연산자를 저장한다.
알고리즘 - sp위치에 피연산자를 저장한다. sp값을 그피연산자 데이터 크기만큼 증가시킨다.
     정의된 명령어를 통해 구현할수 있다. ADD,STORE,indirect 등...을 이용하면 된다.



--함수 반환될때
-sp를 fp위치로 이동시키고 fp값을 sp위치의 데이터값으로 변경한다.

sp와 fp의변화과정

sp
fp
fct2반환전
32
20
fct2반환
20
8
fc1반환
8
0


블로그 이미지

pringlee

하드코어보단 캐주얼!

,
-Immediate Addressing 즉시 주소 지정 방식
operand 가 instruction에 포함된다.
ex)  Add r3 r1 r2 
     : operand인 r3 r1 r2가 함께전달된다

장점 :  여러번 실행없이 한번의 연산으로 읽어서 사용하기 때문에 빠르다.
단점 :  operand를 같이 보내기때문에 operand크기 제약이있다.


-Direct Addressing 직접 주소 지정 방식
instruction의 operand에 직접적인 주소가 들어간다.
ex) Load r1, 0x10
    : 0x10이라는 직접적인 주소가 들어간다.
단점 : 16bit cpu라고하면 8bit정도가 명령어,예약필드로 쓰였다면 8bit주소밖에 표현못한다. 주소공간 제한.

-Indirect Addressing 간접 주소 지정 방식
위의 문제를 해결하기위한 하나의 방식이라고 할수있다.

16bit 레지스터이고

int a=10; //0x0010번지 할당
int b = 20; //0x0100번지 할당
int c = a+b; //0x0020번지 할당
을 하고싶다고 해보자.

LOAD r1,0x10
LOAD r2,0x0100
//안됨!!

여기서 문제가있다. 예약,명령어필드가 4bit이고 첫 operand r1이 4bit라 하자.
0x100이 8bit(0xff)가 넘어가기 때문에 이 주소값은 표현할수없기 때문이다.

RISC cpu의명령어는 대체로 복잡한 연산의 명령어를 제공하지 않는다.
우리가 디자인한 cpu는 LOAD ,STORE ,사칙연산이 전부다. 레지스터에 뭘저장하거나 하는 연산도없다.
따라서 사칙연산으로 0x100을 만들어보자

mul r0,4,4
mul r2, 4,4
mul r3, r0 ,r2
4bit/ 4bit/4bit/4bit
//0x100만들기!

이제 LOAD r2,r3 하고 싶지만(실제값 메모리값 대신 레지스터 지정방식) r3위치에 항상 메모리의 주소가 오게끔 설계했다고 하자. 

//~
잠깐 다른생각을 해보면 r3위치에 레지스터도 오게끔 했다면 어떤 단점이있을지 생각해보자.
레지스터와 메모리주소를 구분하기위해 1bit로 구분지어야한다. 메모리 표현이 3bit로 적어진다. 또한 그것을 구분하기 위해 cpu연산에 있어서 분명 어떠한 비용이 들것이다. 이와같이 설계했을때 장점과 단점을 잘 생각해보아야한다.
~//


r3에는 0x100이 들어있고
나는 LOAD r2,0x100을 하고싶다... 어떻게 하면좋을까?

STORE r3, 0x0030  //임시로 r3에있는 값을 적당한 아무곳에 저장
LOAD r2, [0x0030]  //indirect모드로 LOAD시키기
//메모리를 두번참조한다. 느리다..
0x0030으로가서 0x0100값을 보고 다시 0x0100으로간다.

이제나머지연산...
ADD r3 r1,r2
STORE r3,0x20


레지스터로 주소넘기는 LOAD r2,r3 방법이 편한것같기도 하다. 지금은 추상적이라 모르겠지만 요약하자면 명령어 설계시 다양한 방법이있고 속도측면과 복잡도 측면을 생각하여 설계해야 할것이다.


블로그 이미지

pringlee

하드코어보단 캐주얼!

,
"사칙연산결과를 레지스터에만 저장할 수 있도록 하겠다." 라는 제약사항이 있는데,
그렇다면 메인메모리의 주소값을 통한 사칙연산은 할수없다.
따라서 메인메모리에 저장된 데이터를 레지스터로 일단 옮겨놓은후 사칙연산을 하여야한다.
[메모리 -> 레지스터 ]: LOAD  [레지스터 -> 메모리 ]: STORE

명령어 디자인


Load 명령어 디자인을 해석하면 "destination(레지스터) 에 source(메모리 주소값)에있는 데이터를 저장해라" 라는 명령어이다.
STORE도 마찬가지로 "destination(메모리)에 source(레지스터)에있는 데이터를 저장해라" 라는명령어이다.

예)

int a=10;  //0x10번지할당
int b=20; //0x20번지할당
int c=0; //0x30번지할당
c= a+b;
위와같이 프로그래밍되었다면 

LOAD r1,0x10
LOAD r2,0x20
ADD r3,r1,r2
STORE r3,0x30

위순서대로cpu는 명령을 진행할것이다.

하지만 여기서 ADD때와같은 문제점이있다.
주소값을 표현할때 해봐야 8비트(0~255)밖에 표현이 안된다는 것이다. 
cpu 데이터버스크기가 16비트인데 메모리 주소공간을 8비트만 쓸것인가? 
이에대한 답은 컴퓨터구조 -(4) 에 실어두었다.

블로그 이미지

pringlee

하드코어보단 캐주얼!

,

특수한 목적으로 사용하기위한 레지스터 r4,r5,r6,r7


레지스터 크기를 16비트라고 해보자 명령어 기본구조도 16비트로 구성하는것이 좋겠다. 2의 16승가지의 명령어를 만들수있다.
하지만 2의 16승인 65536개의 명령어는 낭비이다. 실제로 cpu연산에 있어서 몇개 안되는 명령어가 대부분의 연산을 차지한다.


이에따라 명령어 기본구조를 디자인해보자.



레지스터 r1에있는값과 숫자 7을 더해서 레지스터 r2에 저장하라
피연산자는 숫자나 레지스터가 올수있는데 피연산자가 보시다싶이 4bit면
맨앞비트는 레지스터인지 아닌지
나머지 3비트로 식별하거나(총 8개레지스터 식별가능), 숫자를 표현하는데 이는 숫자 0~7까지밖에 표현을 못한다는 단점이있다.


위와같지 실제 명령어 디자인 하는과정에서 제약사항이 등장한다.
예로 arm이나 x86계열 은 "첫번째 피연산자 위치에는 레지스터 이름이 와야한다는 제약사항이있다."
이유는 그저  명령어 구조가 간단해지고 그에따른 cpu의 종합적 측면(비용,속도 등..)에서 좋기 때문이다.


RISC ? CISC ?
-RISC (reduce ...) : 명령어 길이가 동일하고 명령어를 처리하는 과정이 일정하기 때문에 빠르다 대신 명령어 개수가 적다(위의 그림처럼 설계). 파이프라이닝을 통해 효율적이다.
 Fetch Decode Exe
F D E
   F D E
      F D E
         F D E
-CISC (complex ...) : 명령어 개수가 많고 다양하다.

CISC구조에서 전체명령어중 10%정도의 명령어밖에 주로사용되지 않기때문에 이를 착안하여 명령어 개수를 줄여 효율적으로 만든것이 RISC 요즘 cpu는 모두 RISC구조


블로그 이미지

pringlee

하드코어보단 캐주얼!

,

하드웨어의 구성


- 2와 3을 덧셈한다고 한다면 ?

1.하드에있는 실행파일을 메모리에 로드
2.레지스터에 덧셈/2/3 저장
3. 컨트롤유닛이 덧셈이라는 명령을 해석
4. 컨트롤 유닛이 ALU에게 덧셈을 명령
5. ALU는 레지스터에 저장된 2와 3을 덧셈함

'구성요소들'

CPU : 중앙처리장치, 연산담당

    -ALU :  cpu내부에 실제 연산을 담당하는 블록
    
    -컨트롤유닛 : 바이너리 코드를 해석하여 명령어(add,div...등)를 알고 ALU에게    
       그에  따른 일을 시키는 장치
    
    -레지스터 :  cpu연산할때 필요한 데이터가 저장되는곳 이게없다면 cpu가 연산도        중 들어온 데이터를 처리할수없음 또한 데이터 용도에따라 나뉘는게 일반적
    
    -버스 인터페이스 : 입출력 통신방식을 이해하고 그에따라 맞는 통신을 하기위한         장치 ,cpu뿐만아니라 I/O버스와 연결되는 디바이스들은 모두 인터페이스가 필요

메모리(램) : 프로그램이 실행될때 로드되는곳

입출력버스 :  데이터를 주고받기위해 사용되는 경로
    
메모리와 cpu사이에서의 버스
    데이터버스: 데이터값을 받아온다
    어드레스 버스: 데이터를 읽거나 쓸때 해당하는 물리주소를 전달한다.
    컨트롤 버스: cpu가 원하는 바를 메모리에 전달할때 사용
    ex) a와 b를 더해서 c에쓸때:
    데이터 버스 : Add(명령) , a(숫자) , b(숫자)
    어드레스 버스 : c의주소값
    컨트롤 버스 : write
    이런식일것같다.

-클럭신호란?

한번에 클럭신호가 주어지면 디바이스들은 연산을 한번 진행한다. 클럭속도가 너무빠를경우 데이터 손실이 일어난다.

 초당 10번 동작할수있는 A와 초당 5번 동작할수있는 B가있다고하자.
클럭속도를 초당 10으로 맞추면 A가 2번동작할때 B는 한번동작한다.
B가 A가처리한 데이터를 가지고 연산한다면 데이터손실이 일어날것이다.
(A는 두개보냈는데 B는 하나만연산함)
따라서 클럭속도를 느린 장치에 맞추는것이 일반적이다.

블로그 이미지

pringlee

하드코어보단 캐주얼!

,
arch/x86/kernel/syscall_table_32.S로가면
시스템콜 함수들이 나열되어있다.



1.sys_write 변경해보기


유저모드에서는 어떤곳에 출력(write)하고 싶다면 항상 커널에게 요청해야한다. 
-> write()로 시스템콜을 요청하여 sys_write함수를 이용하여 모니터에찍음  

커널모드로 진입한 커널내에서 모니터에 간단히 찍고싶을때
printk이용
-> printk는 커널함수이기 때문에 시스템콜을 요청하는것이아니라 그냥 함수를 실행하여 출력해줌.

쉘에서 printk되는것을 보고싶다면 명령어로
echo 8 > /proc/sys/kernel/printk 를 이용하여 printk 를 모니터에 출력되게 해야함.



무언가 write될때마다 계속 모니터에 찍힘




2.시스템콜 추가해보기


sys_ni_syscall은 현재 사용되지않는 시스템콜이다.
이를 적절히 수정하여 시스템콜을 변경해보자.

(1).시스템콜 테이블에 등록하기

sys_ni_syscall 이름을 내가만들 함수의 이름으로 바꿔준다. 17번에 만들어줌


(2)


커널 컴파일시 이용되는 .c 파일중 아무데나 똑같은 이름으로 만들어준다.

(3)시스템콜 확인





별 쓸모없어 보이는데 다음문제를 생각해보자.
만약 커널내의 전역변수로 할당된 어떤 변수에 접근하고싶다면 어떻게 해야할까?

유저모드에서는 당연히 커널내의 함수는 물론이고 변수접근 할수없음.
적절한 시스템콜함수를 만들어서 시스템콜을 호출해주어야함 .
만약 시스템콜을 이용하여 커널내의 전역변수의 주소값 리턴하여 가져왔다면?
아마 런타임에러가날것임. 유저모드에서는 커널내의 전역변수(특정 로컬 주소값을 지님)접근이 불가함.

이처럼 유저영역과 커널영역은 나누어져있고 유저레벨에서 커널레벨로 접근하려면 꼭 시스템콜을 이용하도록 설계되어있다.
어느곳에 write하고싶으면 항상 write함수를 이용할수밖에없는것이다.

라이브러리로 제작된 printf,scanf 나 파일입출력 라이브러리나  안에 들여다보면 모두 write,read 로 시스템콜을 호출한다.






블로그 이미지

pringlee

하드코어보단 캐주얼!

,
kernel_init은 처음 부팅될때 돌아가는 함수

init/main.c에찾아볼수있다.



int switch=0;
void kernel_init(){
    ......
    do_basic_setup();
    my_atk_func();//
    ......
}

my_atk_func에는 무한루프로 1초씩 msleep를하면서switch가 on이됬는지 검사


에서 키보드입력이 들어오면 switch를 on시키도록
if( code==19)
{
    switch=1;
}


결과





설명:
커널초기화중일때 무한루프상태로 msleep(1) 씩주기로 프로세스가 강제로 스위칭되어 바뀌며 switch 1인지 검사
->계속 검사하던도중 내가 키보드 r을 입력하면 키보드인풋으로 키보드 인터럽트가 날라옴 cpu는 하는일 중단하고 먼저 인터럽트처리 
->그 인터럽트 처리함수로가서 실행되는데 이때  값이 내가 설정해둔 값을때switch  0에서 1로바꿈
->인터럽트처리 끝나서 우선스케줄링된 프로세스부터 실행(인터럽트전 중단된프로세스 일수도있고 알고리즘에따라 다름)
->아까중단된 프로세스가 스케줄되서 kernerinit안에 my_atk_func 함수는 if문을 탈출하게 된다.



블로그 이미지

pringlee

하드코어보단 캐주얼!

,
drivers/input/keyboard/atkbd.c
에있는 atkbd_interrupt함수를 그냥 바로 return 시켜보자


이상태에서 키보드입력을 할수가없음.


drivers/input/keyboard/atkbd.c 에서 code값을 하나증겨시켜버리자

root를 치면 tppy가 쳐진다. 키보드번호의증가때문







블로그 이미지

pringlee

하드코어보단 캐주얼!

,
인터럽트발생 -> idt_table을 참조하여 해당인터럽트 실행
1.시스템콜  -> 시스템콜 테이블참조하여 시스템함수실행
2.인터럽트번호 32~255-> irq_desc참조하여 그 함수실행


ex) 키보드입력
키보드 눌림 -> 인터럽트33번 cpu에게전달 -> cpu는처음 운영체제가 시작할때 메모리에 할당해준 IDT를 참조 -> 33번이기때문에 irq_desc로감 -> 33번에해당하는 irq_desc[1]을 부름-> irq_desc[1] 는 atkbd함수를 호출 -> atkbd함수실행 



128번은 system_call 인터럽트 이고 타이머인터럽트,페이지폴트,키보드입력(그림에는없지만 아마 33)은 다른번호에 배정받아있다.




System_call일때는 IDT넘버가 128번이고 sys_call_table을 참조하여 그에해당하는 커널함수를 알수있다.


fork가 실행될때 간단설명 
  1.  mov eax  4           //시스템콜번호를 레지스터에저장
    INT 128                 
  2. IDT[128]                 //idt테이블에서 128번으로가라 = system_call()
  3. sys_call_table[4]         //sys_call_tale 4번으로가서 실행해라.





블로그 이미지

pringlee

하드코어보단 캐주얼!

,


-커널영역 vs 유저영역


인터럽트는 cpu에게 커널영역의 시스템함수를 호출한다.

cpu는 유저모드 커널모드가 있고, 커널영역의 코드를 실행하기위해서는 cpu는 커널모드로 변경되어야한다. 이를 나눈이유는 커널은 시스템관련 중요한 함수들과 변수가 있기 때문에 함부러 접근할수 없게 하기위함이다.


printf는 라이브러리의 일종이고 안에 여러가지 알고리즘이 있지만 결국에 모니터에 찍을때에는 write라는 시스템함수을 호출하여 인터럽트를 날려 cpu에게 커널영역에있는 함수를 실행해달라고 요청한다.


printf호출(유저영역) ->write호출  -> cpu는 커널모드로 변경 -> write에해당하는 함수실행 ->write끝남-> 유저모드변경 ->다음실행 


-System call Interrupt


read,write호출시 cpu는 커널모드로 바뀌고 커널 주소공간에 있는 코드를 실행하여야 한다. 이는 cpu에 인터럽트를 요청하는것

으로도 볼수있음.소프트웨어 인터럽트이고. system call에서 자세히 다룰것이다.


앞에서 말한 인터럽트처리가 


주프로그램 -> 인터럽트발생(..키보드입력) -> 인터럽트처리 ->주프로그램으로 복귀 


이에반해 system call interrupt는 프로그램이 명령어를 실행하면서 interrupt를 거는것이기때문에 순차적으로 


주프로그램 -> 실행되다가  write를 호출 -> cpu는 커널모드로 바뀌고 write에대한 명령처리 라고봐도 무방하다.


코드를 중단하고 끼어드는 느낌은 아니라고 말하고싶다.


-Hardware Interrupt

인터럽트가 걸리면 현재 실행중인 기계어를 중단하고 해당 인터럽트에 대한 처리를한다.

ex)키보드입력,마우스입력등...


-exception interrupt

0으로나누기, 할당되지 않은주소접근 등 치명적인 오류를 막기위한 인터럽트

블로그 이미지

pringlee

하드코어보단 캐주얼!

,