from bs4 import BeautifulSoup
import requests
import os
import shutil #rmtree 사용하기 위한 모듈
headers = {'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'}
#크롤링 우회하기 위한 헤더 수정
url = "https://comic.naver.com/webtoon/list.nhn?titleId=654774" #소녀의 세계 웹툰 이름, 회차 목록 확인
html1 = requests.get(url, headers = headers)
result1 = BeautifulSoup(html1.content, "html.parser")
webtoonName = result1.find("span", {"class", "wrt_nm"}).parent.get_text().strip().split('\n')
#class가 wrt_nm인 span태그의 부모 태그의 text가져와서 앞뒤 공백 제거 후 개행문자로 나눠서 배열에 저장
os.chdir(r"C:\Users\SAMSUNG\Desktop")
if os.path.isdir(r"C:\Users\SAMSUNG\Desktop\\" + webtoonName[0]): #이미 디렉토리가 있을 경우
shutil.rmtree(r"C:\Users\SAMSUNG\Desktop\\" + webtoonName[0]) #지정된 디렉토리와 하위 디렉토리까지 모두 삭제
os.mkdir(webtoonName[0]) #웹툰 이름으로 디렉토리 만들기, webtoonName 배열의 인덱스 0이 웹툰 이름
print(webtoonName[0] + " folder created successfully!")
os.chdir(r"C:\Users\SAMSUNG\Desktop\\" + webtoonName[0]) #웹툰 제목 디렉토리로 이동
title = result1.findAll("td", {"class", "title"}) #class가 title인 td태그 모두 찾음
for t in title:
os.mkdir((t.text).strip()) #웹툰 디렉토리 안에 회차별로 디렉토리 만들기, text가져와서 앞뒤 공백 제거
os.chdir(os.getcwd() + "\\" + (t.text).strip()) #회차별 디렉토리로 이동
url ="https://comic.naver.com" + t.a['href'] #회차 링크
html2 = requests.get(url, headers = headers) #헤더 우회해서 링크 가져옴
result2 = BeautifulSoup(html2.content, "html.parser") #html로 읽겠다
webtoonImg = result2.find("div", {"class", "wt_viewer"}).findAll("img") #class가 wt_viewer인 div태그 찾고 거기서 img태그 모두 찾음
num = 1 #이미지 저장 명
for i in webtoonImg:
saveName = os.getcwd() + "\\" + str(num) + ".jpg" #저장될 위치 + 저장 명 + 확장자
with open(saveName, "wb") as file: #파일 열기, 이미지 저장할 것임
src = requests.get(i['src'], headers = headers) #헤더 우회해서 이미지 src가져옴
file.write(src.content) #이미지 저장
num += 1 #저장 명 + 1
os.chdir("..") #전 디렉토리로 이동
print((t.text).strip() + " saved completely!") #한 회차 이미지들 저장 완료
네이버 웹툰 이미지 크롤링은 웹 사이트에서 웹크롤링봇을 감지하고 차단하는 기능이 있었기 때문에 크롤링 하기위해서는 헤더를 우회하는 과정이 필요했다.
202번지에 있는 SUB X, Y는 X에서 Y를 빼라는 명령어이다. 뺀 값이 0이 아니라면 그대로 가다가 210번지에서 BR 명령어를 만난 후 무조건 202번지로 분기할 것이다. 하지만 뺀 값이 0이라면 203번지에 있는 BRZ 명령어를 만났을 때 조건코드가 0이기 때문에 211번지로 분기한다. 이후 225번지의 BRE 명령어를 만나 레지스터 R1과 R2 내용이 같은지 비교하고, 같다면 235번지로 분기하고 다르다면 226번지로 갈 것이다.
2. 서브루틴
서브루틴은 한 블록으로 구성된 명령어 실행 중에 또 다른 블록으로 구성된 명령어를 삽입하여 실행하는 형태로 호출(CALL)과 복귀 명령어(RET)가 함께 사용된다.
CALL 명령어는 함수를 호출하는 명령어로 돌아와야할 주소를 스택에 저장하고, PC에는 호출될 서브 루틴의 시작 주소를 저장한 후 분기한다. 여기서 스택에 어디로 돌아갈지에 대한 데이터를 넣는 것을 push라고 한다.
RET 명령어는 분기하기 전 프로그램으로 되돌아가는 명령어로 스택에 저장되어있던 주소값을 PC에 넣어 CALL명령어 바로 다음 주소로 되돌아 간다. 여기서 스택에 저장되어있던 주소값을 가지고 오는 것을 pop이라고 한다.
여기서 짚고 갈 것은 push는 어떤 데이터를 넣을지 데이터가 추가적으로 필요하다. 돌아갈 주소가 추가적인 데이터라고 이해하면 된다. 하지만 pop은 추가적인 데이터가 필요없다. 스택은 후입선출구조이기 때문에 무조건 맨 위의 데이터를 가져오기 때문이다.
210번지에서 서브루틴 SUB1을 호출하면 현재 주소 + 1 = 211을 스택에 넣는다. 돌아왔을 때 210번지의 CALL SUB1을 만나면 무한대로 호출만 하기 때문이다. 그리고 SUB1의 시작주소 즉 250번지의 값을 pc에 넣고 250번지로 분기한다.
이후 260번지에서 서브루틴 SUB2를 호출했기 때문에 260 + 1 = 261을 스택에 넣고 SUB2의 시작주소인 300번지의 값을 pc에 넣고 300번지로 분기한다.
SUB2를 다 돌고 RET명령어를 만나면 다시 *스택포인터가 가리키는 주소를 pc값에 넣고 돌아온다. 261번지로 돌아와 명령어를 수행하던 도중 다시 280번지에서 SUB2를 호출하면 281을 다음 주소인 281을 스택에 넣고 SUB2의 시작주소인 300번지로 분기한다.
SUB2에서 RET를 만나 SUB1로 와서 RET를 만난 후 최종적으로 주프로그램의 END까지 만나면 프로그램이 종료된다.
*sp는 스택포인터로 스택의 맨 위를 가리킨다. RET 명령을 만나면 pop한 후 sp는 하나 밑으로 간다.
3. 명령어 분류
오퍼랜드에 저장되는 데이터의 형태에는 4가지가 있다.
주소
주기억장치의 주소나 레지스터의 주소
수
정수, 고정-소수점 수, 부동-소수점 수
문자
ASCII 코드
논리 데이터
비트(bit)혹은 플래그(flag)
오퍼랜드의 수에 따라 다음과 같이 3, 2, 1, 0 주소 방식이 있다.
이제 하나씩 자세히 알아보자.
3-1. 0주소 명령어
0주소 명령어는 오퍼랜드 없이 연산코드만 있는 것이다.
모든 연산은 스택의 자료들을 활용하기 때문에 스택머신이라고도 한다.
주소를 사용하지 않아 스택의 내용을 변경하면서 수행하기 때문에 원본 데이터가 없다.
3-2. 1주소 명령어
1주소 명령어는 주소의 형태 오퍼랜드 1개를 가진다.
어셈블리 언어) LOAD X ;AC <- M[X]
*어셈블리어는 ;이 주석이다.
LOAD는 연산코드 X는 기억장치 주소로 X를 가지고 오라는 명령어이다.
M은 메모리를 말하고, []는 포인터같은 역할로 X번지의 데이터를 누산기(AC)에 저장하라는 것이다.
3-3. 2주소 명령어
2주소 명령어는 오퍼랜드 2개를 포함하는 명령어 형식으로, 둘다 주소를 저장한다.
주로 목적지에 다시 값을 넣음으로 목적지에 있던 원본 데이터가 없어진다.
어셈블리 언어) MOV X, Y ;M[X] <- M[Y]
MOV는 이동시키는 명령어로 X가 목적지주소, Y가 소스주소이다. 즉 Y번지의 기억장치 데이터를 X번지의 기억장치로 이동시킨다.
3-4. 3주소 명령어
3주소 명령어는 오퍼랜드 3개를 포함하는 명령어 형식으로, 레지스터 주소들을 저장한다.
원본 자료를 파괴하지 않으며 프로그램 전체의 길이가 짧아지지만 명령어 한 개의 길이가 너무 길어진다는 단점이 있다.
또한 하나의 명령을 수행하기 위해 최소 4번의 기억장소 접근이 필요하여 수행시간이 길어진다.
어셈블리 언어) ADD X, Y, Z ;M[X] <- M[Y] + M[Z]
ADD는 더하라는 명령어로 뒤에서 부터 읽어 앞에 넣는다. X가 목적지 주소, Y가 소스2 주소, Z가 소스1 주소이다. 즉 Y와 Z번지의 데이터를 덧셈해서 X번지에 저장하는 것이다.
*목적지 주소가 있는 오퍼랜드의 위치는 cpu제조사 마다 다를 수 있음!
3-5. 4가지 주소 형식을 사용한 수식 연산 프로그램
X = B * (C + D * E - F / G) 연산 프로그램을 구현후 비교
*사용할 명령어들에 대한 정리
명령어
동작
ADD
덧셈
SUB
뺄셈
MUL
곱셈
DIV
나눗셈
MOV
데이터 이동
LOAD
기억장치로부터 데이터 적재(데이터 가져옴)
STOR
기억장치로 데이터 저장
3-5-1. 0주소 명령어를 사용한 프로그램
0주소는 push랑 pop만쓰고 STOR을 쓰지 않는다.
X = B * (C + D * E - F / G)
0주소 명령어가 실행될 때 스택의 내용을 보자.
피연산자는 PUSH하고, 연산자를 만나면 연산에 사용할 피연산자를 POP한 후에 연산 결과를 PUSH한다.
여기서 B와 C를 바로 곱하지 않는 것은 괄호가 들어갔기 때문이다. 괄호가 들어오면 플래그 레지스터가 활성화 되어서 여는 괄호가 닫는 괄호를 만날 때까지 수행하지 않는다.
3-5-2. 1주소 명령어를 사용한 프로그램
X = B * (C + D * E - F / G)
여기서 AC는 누산기이고 M[A]는 기억장치 A번지에 저장된 데이터 내용을 가리킨다.
106번지 STOS T에서 STOS는 Store String으로 EDI가 가리키는 주소에 EAX값을 저장한다.
T는 기억장치 내의 임시 저장장소의 주소를 나타낸다.
1주소 명령어는 항상 기준이 누산기이다. 누산기에 있는 값을 연산한 후 다시 누산기에 넣는다.
3-5-3. 2주소 명령어를 사용한 프로그램
X = B * (C + D * E - F / G)
2주소는 명령어 A, B에서 A가 목적지, B가 소스 주소이다.
M[A]는 기억장치 A번지에 저장된 데이터 내용이며 R1, R2는 cpu내의 위치한 레지스터이다.
3-5-4. 3주소 명령어를 사용한 프로그램
X = B * (C + D * E - F / G)
3주소는 명령어 A, B, C에서 A가 목적지, B가 소스1, C가 소스2 주소이다. 여기서 주의해야할 점은 DIV나 SUB같은 명령어처럼 소스의 순서가 바뀌면 아예 값이 바뀌는 경우이다. 꼭 소스1에서 소스2를 연산하도록 해야한다.
*목적지 주소가 있는 오퍼랜드의 위치는 cpu제조사 마다 다를 수 있음!!
3-5-5. 명령어 주소 개수에 따른 장단점
많은 주소를 쓰면 전체적인 명령어 프로그램 길이가 짧아진다. 대신 하나의 명령어 처리 속도가 느려진다. 반면에 주소 개수가 적으면 명령어 인출과 실행 속도가 높아지며 프로그램 길이가 길어진다. 예를 들어 0주소는 가져오는 것, 더하는 것, 넣는 것 모두 따로 있다면 3주소는 가져와서 더하고 넣은 것이 하나의 명령어에 있기 때문이다.
BRZ는 조건코드가 0이면 분기하라는 명령어이고 BRE는 레지스터의 내용이 같다면 분기하라는 명령어로 둘 다 조건코드가 있기에 조건 분기를 한다. 하지만 BR은 무조건 분기하라는 명령어로 무조건 분기를 하며 BRT와 BRA는 분기 명령어가 아니다.
2. CALL, RET
3. (A)261, (B)300 스택에는 돌아올 주소가 들어가야한다. 현재 260번지이므로 돌아왔을 때는 260 + 1인 261번지로 와야한다. pc값은 다음으로 갈 주소가 있어야한다. CALL SUB2명령어는 서브루틴 SUB2를 호출하라는 뜻으로 SUB2의 시작주소 300이 들어가야한다.
4. 0주소 명령어 push는 스택 명령어로 스택을 사용하는 0주소 명령어에서 쓰인다.
5. 1주소 명령어 AC는 누산기이다. 연산값을 누산기로 다시 넣는 것을 보아 누산기를 쓰는 1주소 명령어이다.
프로그램이 실행되기 위해서 운영체제로부터 메모리 공간(RAM)을 할당받는다. 메모리 구조는 아래와 같이 데이터, 코드, 스택, 힙 영역을 가진다.
코드영역이 가장 낮은 주소이며 스택영역이 가장 높은 주소이다.
여기서 중요한 개념이 있는데 힙 영역과 스택 영역은 사실 같은 공간을 공유하는 것이다. 그래서 힙은 낮은 주소 즉 위쪽 주소부터 할당되고 스택은 높은 주소 즉 아래 쪽 주소부터 할당되는 것이다. 하지만 이 둘이 같은 공간을 공유하기 때문에 서로의 영역에 침범하는 일이 발생할 수 있는데 힙이 침범하면 힙 오버플로우, 스택이 침범하면 스택 오버플로우라고 한다.
스택 영역이 크면 힙 영역이 작아지며, 힙 영역이 커지면 스택 영역이 작아진다.
1-1. 코드 영역(=텍스트 영역)
-프로그램의 코드가 저장되는 영역으로 소스코드가 들어가는 부분이다. 실행파일을 구성하는 명령어 즉 함수, 제어문, 상수 등이 저장된다. 또한 프로그램이 시작하고 끝날 때까지 메모리에 계속 남아있다.
1-2. 데이터 영역
프로그램의 전역 변수, 정적(static) 변수, 문자열 상수가 저장되는 영역으로 프로그램의 시작과 동시에 할당되며 프로그램이 종료되기 전까지 메모리에 있다.
1-3. 스택 영역
프로그램이 자동으로 사용하는 임시 메모리 영역으로 함수 호출 시 생성되는 지역변수와 매개 변수가 저장되는 영역으로 이 변수들의 특징과 같이 함수가 끝나면 사라진다. 컴파일할 때 크기가 결정되며 메모리의 높은 주소에서 낮은 주소의 방향으로 할당 된다. 위 사진에서 처럼 화살표 방향으로 할당된다.
1-4. 힙 영역
프로그래머가 할당 및 해제하는 공간이다. 이 공간에 할당하는 것을 동적 할당이라고 하며 malloc함수 또는 new 연산자를 통해 할당되며 free또는 delete에 의해 해제 된다. 힙 영역은 메모리의 낮은 주소에서 높은 주소의 방향으로 할당되는 것도 특징이라고 할 수 있으며 런타임 할 때 크기가 결정된다.
2. 레지스터 종류
2-1. 범용 레지스터
EAX(Extended Accumulator Register)
덧셈, 곱셈, 나눗셈 등 산술 연산과 논리 연산을 수행하며 함수의 반환값이 이 레지스터에 저장되며 함수의 성공, 실패 여부를 파악할 수 있으며 반환값을 가져올 수 있다. 예를 들어 return 10을하면 EAX에 10이 입력된다고 생각하면 된다.
EBX(Extended Base Register)
ESI나 EDI와 결합될 수 있으며, 메모리 주소를 저장하기 위한 용도로 쓰인다.
ECX(Extended Counter Register)
반복 명령어를 사용할 때 반복 카운터로 사용되는 레지스터이다. for, while문과 같은 반복문 수행할 때 카운팅하는 역할이다.
EDX(Extended Data Register)
EAX와 함께 쓰이며 큰 수의 곱셈 또는 나눗셈 등의 연산이 이루어질 때 몫은 EAX에 나머지는 EDX에 저장된다.
ESI(Extended Soource Index)
데이터 복사, 조작할 때 사용되며 복사의 원본 주소를 저장한다.
EDI(Extended Destination Index)
데이터 복사, 조작할 때 사용되며 복사의 목적지 주소가 저장된다.
ESP(Extended Stack Pointer)
스택의 끝 주소(가장 최근에 저장된 것)가 저장되며 push, pop 명령에 따라 값이 4바이트씩 변한다.
EBP(Extended Base Pointer)
스택의 시작 주소(가장 처음으로 저장된 것)가 저장되며 값은 고정이다.
2-2. 명령 포인터
EIP(Extended Instruction Pointer)
다음 실행할 명령어가 존재하는 메모리 주소가 저장된다.
2-3. 세그먼트 레지스터
세그먼트는 프로그램에 정의된 메모리 상의 특정 영역이다. 자신에게 지정된 주소를 가리키는 것이 특징이다.
CS(Code Segment)
코드 세그먼트의 시작주소를 가리킨다.
DS(Data Segment)
데이터 세그먼트의 시작주소를 가리킨다. 명령어는 이 데이터 세그먼트를 사용해서 데이터의 위치를 알아낸다.
SS(Stack Segment)
스택 세그먼트의 시작주소를 가리킨다. 메모리 상에 스택 구현을 가능하게 한다.
2-4. 플래그 레지스터
CF(Carry Flag)
부호 없는 연산 결과가 용량보다 클 때 1이 된다.
ZF(Zero Flag)
산술 및 논리 연산의 결과가 0일 때 1이 된다.
OF(Overflow Flag)
정수형 결과 값이 너무 큰 양수거나 너무 작은 음수여서 오버 플로우가 발생할 때 1이 된다.
SF(Sign Flag)
연산 결과가 음수면 1이 된다.
PF(Parity Flag)
논리적으로 1인 비트의 수가 홀수인지 짝수인지를 신호하는 지시자로, 1인 비트 수가 짝수개면 1이 되고 그 이외의 경우 0이 된다.
명령어를 효과적으로 실행하기 위한 기법에는 주소 지정 방식, 파이프 라인 그리고 인터럽트가 있다.
1. 주소 지정 방식
주소란 주기억창치에서 데이터가 저장된 위치를 가리킨다. 주소 지정 방식은 주소를 지정하는 방식에따라 직접 주소 지정 방식, 간접 주소 지정 방식, 레지스터 주소 지정 방식, 레지스터 간접 주소 지정 방식, 변위 주소 지정 방식이 있다.
주소 지정 방식에서 쓰이는 내용과 그 내용에 대한 표기법을 알아보자.
정의 내용
표기 방법
유효 주소(기억장치의 실제 주소)
EA
기억장치 주소
A
레지스터 번호
R
기억장치 A번지의 내용
(A)
레지스터 R번지의 내용
(R)
1-1. 직접 주소 지정 방식
-가장 일반적인 개념의 간단한 주소 방식으로 주소 필드에 유효 주소를 직접 저장한다.
-유효주소가 기억장치 주소이며 EA=A로 나타낼 수 있다.
-특징: 화살표가 한 번만 있어서 한 번만 접근하기 때문에 데이터를 빠르게 가지고 올 수 있다는 장점이 있지만, 16비트의 명령어중에서 연산코드가 4비트를 차지하고 있을 경우 오퍼랜드는 총 길이에서 연산코드길이를 뺀 12비트만을 사용할 수 있기 때문에 2^12까지의 주소만 사용할 수 있어 제한적이라는 단점이 있다.
2-2. 간접 주소 지정 방식
-유효주소가 기억장치 A의 내용이며 EA=(A)로 나타낼 수 있다. (A)는 포인터의 개념처럼 A라는 포인터가 가리키는 곳의 값이라고 이해하면 된다.
-직접 주소 지정 방식과는 다르게 기억장치에 두 번 접근한다. 처음 접근하는 곳은 유효주소의 주소를 가지고 있으며 두 번째로 접근하는 곳에는 실질적인 데이터가 있는 것이다.
-특징: 직접 접근 방식의 단점을 보완한 방식으로 오퍼랜드의 길이에 제한을 받지 않는다. 이 방식은 최대 기억장치 용량에 따라 활용할 수 있는 주소 공간을 확장할 수 있다. 따라서 긴 주소에 접근할 수 있다는 장점이 있다. 하지만 기억장치에 두 번 접근을 해야해서 직접 주소 지정 방식보다 속도가 느리며, 명령어 형식에서 주소 지정 방식을 표시하는 간접 비트 필드가 필요하다.
I가 0이면 직접 주소 지정 방식, 1이면 간접 주소 지정 방식이다.
1-3. 레지스터 주소 지정 방식
-오퍼랜드에 레지스터 번호가 저장되어 있으며 명령어 수행에 필요한 데이터가 해당하는 레지스터에 저장되어 있다. 주기억장치가아닌 레지스터에 데이터가 있다는 것이 중요하다.
-유효주소가 레지스터의 번호로 EA=R로 나타낼 수 있다.
-특징: 오퍼랜드 필드 비트수가 적어도 되며, 기억장치에 액세스 하지 않기 때문에 데이터 인출 시간이 적다는 장점이 있다. 하지만 데이터가 cpu내부의 레지스터에 저장되기 때문에 저장공간이 속도는 빠르지만 용량은 적은 레지스터로 제한된다.
1-4. 레지스터 간접 주소 지정 방식
-레지스터에 저장된 것이 연산에 사용할 실질적인 데이터가 아닌, 그 데이터를 가진 기억장치의 주소이다.
-유효 주소가 레지스터에 저장된 내용을 가리키므로 EA=(R)로 나타낼 수 있다. 간접 접근 지정 방식과 마찬가지로 (R)은 R포인터가 나타내는 곳의 데이터라고 이해하면 된다.
-레지스터의 길이에 따라 주소지정 영역이 결정되는데, 만약 레지스터의 길이가 16비트라면 2^16까지의 주소를 지정할 수 있다.
-특징: 사용되는 레지스터 길이에 따라 많은 주소 공간을 사용할 수 있다는 장점이 있다. 하지만 주기억장치에 직접 2번 접근하는 간접 주소 방식과 달리 주기억장치에는 한 번만 접근해서 메모리 참조가 적게 일어나지만 주기억장치에 접근하기 전 레지스터에 접근해야하므로 여분의 메모리 참조가 필요하다는 단점이 있다.
1-5. 변위 주소 지정 방식
-직접 주소 지정 방식과 레지스터 간접 주소 지정 방식을 조합한 방식이다.
-유효주소가 R이 가리키는 레지스터의 내용과 변위 값 A를 더한 것으로 EA=(R)+A로 나타낼 수 있다. 따라서 두 필드의 조합으로 유효주소가 생성된다.
-16비트의 길이를 가진 레지스터는 2의 16승까지의 주소만 저장 가능하지만 변위값을 더하면 주소값을 더 많이 수용할 수 있기 때문에 이 방식을 사용한다.
-사용하는 레지스터에 따라 상대 주소 지정 방식, 인덱스 주소 지정 방식, 베이스 레지스터 주소 지정 방식으로 나눌 수 있다.
1-5-1. 상대 주소 지정 방식
-프로그램 카운터(PC)값을 사용한다.
-주로 분기 명령어에서 사용되는데 분기명령어는 JUMP와 CALL같이 다른 위치의 명령어로 분기시키는 명령어를 말한다.
-유효 주소는 프로그램 카운터의 내용과 기억장치의 주소를 더한 것으로 EA=(PC)+A로 나타낼 수 있다.
-예를 들어, 120번지에 저장된 JUMP 명령어가 인출된 후에, PC의 내용이 121이 된 경우 A가 +50dlaus 171번지가 되고, A가 -70인 경우 51번지가 된다. 여기서 A값을 음수로 사용할 수 있는 이유는 2의 보수로 음수값이 표현가능하기 때문이다.
1-5-2. 인덱스 주소 지정 방식
-인덱스 레지스터를 사용한다. 인덱스 레지스터란 인덱스 값을 저장하는 특수 레지스터이다.
-유효 주소는 인덱스 레지스터의 내용과 기억장치 주소를 더한 것으로 EA=(IX)+A로 나타낼 수 있다.
-특징: 명령어가 실행될 때마다, 인덱스 레지스터의 내용이 자동적으로 증가 또는 감소하기 때문에 for문 같은 루프 프로그램에서 동일한 명령어를 사용해 배열내의 데이터에 접근하여 연산하는데 효율적으로 사용될 수 있다. 만약 명령어가 실행된다면 EA=(IX)+A와 IX<-IX+1이 연속적으로 수행된다.
1-5-3. 베이스 레지스터 주소 지정 방식
-베이스 레지스터를 사용한다.
-유효 주소는 베이스 레지스터의 내용과 기억장치 주소를 더한 것으로 EA=(BR)+A로 나타낼 수 있다.
2. 명령어 파이프 라인
-하나의 명령어를 여러 단계로 나누어서 단계별로 동시에 수행하여 처리 속도를 향상 시키는 방법이다.
-몇 단계로 나누냐에 따라 2단계, 4단계, 6단계 명령어 파이프라인으로 나눌 수 있다.
-파이프 라인 예시로 좋은 세탁기가 있는데, 세탁물 1, 2, 3이 있다고 하자, 이때 세탁물1의 빨래, 건조, 정리가 다 끝나고 세탁물2를 빨래, 건조, 정리하고 끝나면 또 세탁물3 빨래를 시작하면 시간이 오래걸리며 상당히 비효율적이다. 세탁기, 건조기와 내가 있기 때문에 세탁물이 1개가 모든 과정이 끝날 때까지 기다릴 필요없이 각각 자기 할 일을 수행하면 시간이 훨씬 단축되며 효율적이다.
-파이프 라인은 클록 주기라는 것을 사용하는데 이는 cpu가 명령어를 처리하는 주기이다.
2-1. 2단계 명령어 파이프 라인
-명령어를 인출 단계와 실행 단계라는 두 개의 독립적인 파이프라인 모듈로 분리해서 수행한다. 인출 단계에서 명령어 몇개를, 실행 단계에서 명령어 몇개를 동시에 수행하는 것이 아닌, 인출 단계에서 1개, 실행 단계에서 1개를 동시에 수행할 수 있다는 것이다.
-여기서 명령어 1이 들어가면 먼저 인출 단계를 거친다. 이때, 실행 단계는 아무 것도 수행하고 있지 않는다. 명령어 1이 인출 단계가 끝나면 실행 단계로 들어가게 되는데 그렇게 되면 인출 단계는 아무 것도 안하고 있으니 명령어 2를 받아서 수행한다. 이런식으로 각각의 명령어는 다음 명령어를 받아서 수행한다.
-주의할 점은 인출 단계를 거쳐야 실행 단계로 넘어갈 수 있다는 것이다.
2-2. 4단계 명령어 파이프 라인
-명령어 인출(IF), 명령어 해독,(ID) 오퍼랜드 인출(OF, 실제 데이터를 인출함), 실행(EX) 이렇게 4단계로 구성된 파이프 라인이다.
-2단계 파이프 라인처럼 명령어 인출이 끝나고 명령어 해독 단계로 넘어가면 다음 명령어가 명령어 인출을 시작한다. 클록 주기에서 '4'자리에 보면 명령어1은 4단계, 명령어2는 3단계, 명령어3은 2단계, 명령어4는 1단계를 동시에 수행하고 있는 것을 볼 수 있다.
2-3. 6단계 명령어 파이프 라인
-명령어 인출(FI), 명령어 해독(DI), 오퍼랜드 계산(CO), 오퍼랜드 인출(FO), 명령어 실행(EI), 오퍼랜드 저장(WO) 이렇게 6단계로 구성된 파이프 라인이다.
-2,4단계 파이프 라인과 마찬가지로 하나의 단계가 끝나면 다음 명령어가 그 단계에서 바로 수행된다. 클록 주기에서 '9'자리에 보면 명령어 4는 6단계, 5는 5단계, 6은 4단계 이런식으로 6단계가 각각 다른 명령어를 동시에 수행하고 있다.
*여기서 흥미로운 점은 4단계와 6단계의 같은 단계라도 영어가 다르다는 것이다.
단계 명
4단계
6단계
명령어 인출
Instruction Fetch
IF
Fetch Instruction
FI
명령어 해독
Instruction Decode
ID
Decode Instruction
DI
오퍼랜드 계산
X
X
Calculate Operand
CO
오퍼랜드 인출
Operand Fetch
OF
Fetch Operand
FO
명령어 실행
Execute
EX
Execute Instruction
EI
오퍼랜드 저장
X
X
Write Operand
WO
2-4. 파이프 라인에 의한 속도 향상
-명령어 실행 시간 계산
k
파이프 라인 단계 수(2단계는 2, 4단계는 4, 6단계는 6)
N
실행할 명령어들의 수
각 파이프라인의 단계
한 클록 주기씩 소요됨
T
파이프라인을 적용했을 때, N개의 명령어를 실행하는 데 소요되는 시간
T = k + (N - 1)
T
파이프라인을 적용하지 않았을 때, N개의 명령어를 실행하는 데 소요되는 시간
T` = k * N
-먼저 T = k + (N - 1)의 식을 해석해보자면, k는 첫 번째 명령어를 실행하는 시간이며, (N - 1)은 첫 번째 명령어를 뺀 나머지 명령어들이 소요되는 시간이다. 1*(N - 1)에서 1*을 생략했다고 생각하면 된다.
4단계 파이프라인을 예시로 들어보자.
클록 주기를 초단위로 생각해보면 명령어 1은 4초가 걸리고 나머지 명령어들은 1초씩만 사용한다고 생각할 수 있다. 따라서 4단계 파이프 라인에서 명령어 6개 실행시간 T는 명령어 1이 수행되는 시간 4 + 총 명령어 6에서 1을 뺀 값인 5을 하면 9가 된다.
이와 달리 파이프라인을 적용하지 않았을 경우에는 명령어 1개당 4개의 단계씩 거치지 때문에 명령어 총 개수 * 단계 수를 하면된다. 따라서 파이프 라인을 적용하지 않고 명령어 6개를 실행하는 시간은 4 * 6 = 24이다.
이 둘을 가지고 파이프 라이닝에 대한 속도 향상도 알 수 있는데 파이프 라이닝 하지 않고 소요된 시간 / 파이프 라이닝하고 소요된 시간 즉, T` / T로 나타낼 수 있으며 여기서는 24 / 9 해서 약 2.27배의 속도 향상이 있었다고 말할 수 있다.
3. 인터럽트
-cpu가 현재 실행 중인 프로그램의 처리를 강제적으로 중단시키고, 특정 주소에 위치한 프로그램을 수행하는 것이다.
3-1.동작원리
인터럽트 서비스 루틴(ISR, Interrupt Service Routine): 인터럽트를 처리하기 위해 실행되는 프로그램 루틴이다.
인터럽트가 발생하면 현재 실행하고 있던 프로그램의 중요한 데이터는 주기억장치에 저장되며 중지된다. 중지 된 후 인터럽트 서비스 루틴이 처리하는 프로그램이 종료된 후에 다시 재개된다. 예를 들어 어떤 유튜브 영상을 보고 있는데 친구에게 카톡이 와서 카톡을 확인한 후 다시 유튜브로 가서 아까 보다 멈춘 곳 부터 다시 보는 것이 있다.
3-2. 인터럽트 종류와 발생 원인
1)전원 이상 인터럽트(Power Failed Interrupt): 정전이 되거나 이상이 있는 경우 발생한다.
2)기계 착오 인터럽트(Machine check interrupt): CPU의 기능적인 오류로 발생한다.
3)입출력 인터럽트(I/O Interrupt): 입출력 데이터의 종료나 오류에 의해 cpu기능이 요청되는 경우 발생하며 예로 사용자 입력 요청을 보냈을 때 사용자의 입력이 끝날 때까지 다음 명령어를 수행안하는 것을 들 수 있다.
4)외부신호 인터럽트(External Interrupt): 오퍼레이터나 타이머에 의해 의도적으로 프로그램이 중단된 경우 발생하며 타이머에 의해 설정된 시간을 알리는 경우나 외부 장치로부터 인터럽트 요청이 있는 경우가 있다.
5)프로그램 검사 인터럽트(Program Check Interrupt): 오버플로우나 0에 의한 나누기, 프로그램에서 명령어 잘못사용하는 경우처럼 프로그램 실행 중 보호된 기억공간 내에 접근하거나 불법적인 명령 수행으로 발생한다.
6) 슈퍼바이저 호출 인터럽트(Supervisor Call Interrupt): 사용자가 슈퍼바이저 호출(SVC) 명령어를 써서 운영체제에 서비스를 요청하거나 복잡한 입출력 처리를 해야하는 경우 발생한다.
7)재시작 인터럽트(Restart Interrupt): 콘솔 상에서 재시작 버튼이나 Ctrl-Alt-Del키를 누른 경우 발생한다.
3-3. 인터럽트 사이클
-인터럽트 발생을 처리하기 위한 사이클로 cpu가 인터럽트 요구의 존재 여부를 검사한다.
만약 인터럽트 발생이 없으면 다음 명령어를 인출하면 되지만 인터럽트 요구가 대기 중이면, 진행하고 있던 프로그램을 중단 한 후, 프로그램 상태에 저장한다. 이후 프로그램 카운터를 인터럽트 처리 루틴의 시작 주소로 설정하고 인터럽트를 처리한다.
인터럽트를 처리하기 위해서는 현재 프로그램의 데이터를 저장해야 인터럽트가 끝난 후 다시 재개할 수 있으며 프로그램 카운터를 인터럽트 주소로 해야 다음 명령어 대신 인터럽트를 수행할 수 있다.
3-4. 다중 인터럽트 처리
다중 인터럽트란 인터럽트 서비스 루틴을 수행하는 동안 또 다른 인터럽트가 발생한 것이다. 이 때는 원래 실행하고 있던 인터럽트가 끝난 후에 처리할 지, 또 다른 인터럽트가 발생한 순간 처리하고 원래 실행하던 인터럽트를 처리할 지의 문제가 발생하기 때문에 다중 인터럽트 처리하기 위한 2가지 방식이 있다.
-순차적인 다중 인터럽트 처리
원래 발생했던 x를 처리한 후 y를 처리하는 방식이다. x를 처리할 동안 y는 대기상태에 있다.
-우선순위 다중 인터럽트 처리
인터럽트의 우선 순위를 정한 후, 만약 x 우선순위가 높다면 x가 끝난 후 y를 처리하고, y의 우선순위가 높다면 x를 잠시 중단한 후 y를 처리하고 다시 x를 재개한다.
*우선순위
전원 이상(Power fail) > 기계 착오(Machine Check) > 외부 신호(External) > 입출력(I/O) > 명령어 잘못 > 프로그램 검사(Program Check) > SVC(SuperVisor Call)
직접 주소 지정 방식은 기억장치에 한 번 접근하며,레지스터 간접 주소 지정 방식과 변위 주소 지정 방식은 레지스터에 한 번,기억장치에 한 번 접근한다.
2. 레지스터 주소 지정 방식
유효주소가 레지스터의 번호이다.그 레지스터에는 실질적인 데이터가 들어있다.
3. 1
인덱스 주소 지정 방식은 인덱스 값을 저장하는 특수 레지스터인 인덱스 레지스터를 사용하고 레지스터 주소 지정 방식은 레지스터를 사용하며 베이스 레지스터 주소 지정 방식은 베이스 레지스터를 사용한다.
4. 20
파이프 라인을 적용할 경우 명령어 실행 시간T는 파이프 라인의 단계 수k +실행할 명령어 수N – 1즉T = k + (N – 1)로 나타낼 수 있다.따라서T = 6 + 15 – 1이므로20이다.
5,. 80
파이프 라인을 적용하지 않을 경우 명령어 실행 시간T`는 파이프 라인의 단계 수k *실행할 명령어 수N즉T` = k * N으로 나타낼 수 있다.따라서T` = 4 * 20으로80이다.
6. 6
명령어 파이프 라인을 적용할 경우n번째 명령어는 단계에 상관 없이n번째 클록에서 시작된다.첫 번째 단계가 명령어를 하나씩 처리하기 때문이다.따라서6번 째 명령어는 6번째 클록에서 시작된다.
7. 인터럽트
8. 4
기계 착오 인터럽트는cpu의 기능적인 오류로 발생하며,입출력 인터럽트는 입출력 데이터의 종료나 오류에 의해 나타나고,외부신호 인터럽트는 오퍼레이터나 타이머 또는 외부장치로부터 요청이 오면 발생한다.
9. O
다중 인터럽트가 발생할 경우 우선 순위 다중 인터럽트 처리에서는 우선 순위가 높은 인터럽트부터 먼저 처리하는데 기계 착오 인터럽트가 외부 신호 인터럽트보다 높기 때문에 먼저 수행된다.
10. 2.5
문제5, 6의 해설을 보면 명령어 파이프 라인을 실행한 경우,실행하지 않은 경우에 대한 시간 공식을 알 수 있다.파이프 라인 실행한 경우 속도T는4 + (5 - 1)로8이며 파이프 라인 실행하지 않은 경우 속도T`는4 * 5로20이다. 20 / 8하면2.5배 향상된 것을 알 수 있다.
만약, 기계코드가 8170이면 8은 연산코드에 170은 오퍼랜드 1에 들어가며 오퍼랜드 2는 패딩영역으로 쓴다.
-연산 코드
>함수 연산기능: 산술, 논리 연산 수행
>전달기능: CPU와 주기억장치 사이 정보교환과 적재, 저장
>제어기능: 프로그램 수행흐름 제어
분기 명령어
-오퍼랜드: 다음에 실행할 명령어 주소를 포함
서브루틴 호출 명령어
-호출 명령어: 현재 PC 내용을 스택에 저장하고 서브루틴의 시작주소로 분기함
-복귀 명령어: CPU가 원래 실행하던 프로그램으로 되돌아가도록함
>입출력기능: CPU와 외부 장치들 간의 데이터 이동
-오퍼랜드: 연산을 수행하는데 필요한 데이터 혹은 데이터의 주소
-데이터이동
-mov: 데이터를 이동한다.
-사칙연산
-add: 덧셈
-sub: 뺄셈
-mul: 곱셈
-div: 나눗셈
-기억장치 관련
-load: 기억장치로부터 데이터 적재
-srot: 기억장치로 데이터 저장
5. 어셈블리 프로그램 예제
먼저 기계코드를 설명하자면, 컴퓨터만 이해할 수 있는 코드로 명령어와 주소를 합친 것이다. LOAD명령어를 나타내는 기계코드는 1이고, ADD명령어를 나타내는 기계코드는 5인 것이다.
100번지에 있는 1250은 '250번지의 값을 가져와라'라는 것이다. 그럼 컴퓨터는 250번지에 있는 값을 가져와서 누산기에 넣는다. 이후 101번지에 있는 5251코드로 인해 컴퓨터는 251번지에 가서 값을 가져와 누산기의 값과 더한 후 102번지에 있는 2251코드로 인해 251번지에 값을 저장한다. 이 명령이 끝나면 103번지의 170번지로 점프하라는 기계코드를 실행한다.
이 예시를 단계별로 도식화해보자.
여기서 PC는 프로그램 카운터, AC는 누산기, IR은 명령어 레지스터이다.
LOAD 250
CPU 레지스터의 프로그램 카운터 값이 100번지를 가리키고 있으므로 기억장치의 100번째 주소의 기계코드를 명령어 레지스터에 넣는다. 컴퓨터는 이를 이해하고 누산기에 250번지의 값을 넣는다. 이후 프로그램 카운터값은 101번지를 가리킨다.
ADD 251
프로그램 카운터 값이 101이므로 기억장치의 101번째 주소의 명령어를 해석한다. 이후 251번지의 값을 누산기에 있는 250번지의 값과 더해서 다시 누산기에 넣는다.
STOR 251
프로그램 카운터 값이 102이므로 기억장치의 102번째 주소의 명령어를 해석한다. 이후 251번지에 누산기의 값을 가져와 저장한다.
JUMP 170
프로그램 카운터 값이 103이므로 기억장치의 103번째 주소의 명령어를 해석한다. 이후 170번지로 점프한다.(프로그램 카운터 값이 170으로 바뀐다.)