본문 바로가기
일상,취미

[TIL 33일차] roms 파일 체크 목록....그리고 코드를 대하는 나의 자세

by 진득한진드기 2024. 7. 30.

오늘 한일

window에서 바이너리 데이터를 추출할 일은 거의 없겠지만 romsfsck2 형식 파일을 추출하는 코드의 동작은 굉장히 복잡한거 같다.

 

분석해본 결과 일단 roms에서 사용할 수 있는 항목들을 분류하고 이후에 오류가 날수 있는 부분을  찾아야한다.

 

보통 심볼릭링크와 하드링크, 체크섬 하는 기능과 크기와 아이노드 체크, 파일 재귀적 탐색, 매직 바이트 체크 등에서 오류가 날수 있으므로 위에 항목은 기본적으로 체크해야한다.

 

보통 파일구조마다 커널 코드에 등록되어 있기 때문에 파일 분석 코드들을 보면 매직바이트를 비교하는게 일반적이다.

 

코드로 봐보면 다음과 같다.

 

def verify_checksum(romfs_bytes):
	total = 0
    int32 = 2 ** 32
    
    for i in range(0,len(roms_bytes),4):
    	total = (total + struct.unpack('>L',romfs_bytes[i:i+4][0]) % int32
        
    return total

 

여기서 >L은 빅엔디안형에 부호없는 32비트 정수를 의미한다.

 

32비트 정수의 오버플로우를 처리하기 위해 total 변수에 현재 위치의 4바이트 값을 더한 후 int32로 나눈 나머지를 저장하게 된다.

 

만약 total이 반환될 때 값이 0이면 데이터가 무결하다는걸 의미하고 0이 아니라면 데이터가 손상되었음을 의미한다.

 

그리고 inode의 탐색 시작점은 무조건 디렉토리 구조여야한다는것.

 

구조를 파악하기 위해 타입 검사를 해보고 error로 반환 시키는것도 좋은 방법이다.

 

이를 테면

 

파싱된_타입 = (data_type >> 28) & 0x7

 

와 같이 상위 비트 3개만 가져와서 판단하는 것처럼 말이다.

 

roms는 그나마 추상화가 굉장히 잘되어있는 코드가 많은 파일 중 하나이다.

 

나 같은 경우는 jefferson파일 구조가 좀 보기 더 난해한거 같다.

 

코드가 써있기만 파이썬으로 써있지 그냥 C언어랑 크게 다를건 없다.

 

커널에 등록된 파일시스템 구조를 파악해서 추출하는것이기에......

 

아직 파일 시스템이랑 커널에 대해 아는게 거의 없어서 커널 책도 좀 보면서 해야겠다.

 

실제로 리눅스 커널 공식 사이트에 가면 api이나 다른 설명들이 고전적인건 굉장히 잘되어있다.

 

 

퇴근 후 공부 

 

보통 서버를 개발할 때 2가지를 고민한다.

서버모델을 Thread 기반으로 할건지 Select기반으로 할건지 epoll 기반으로 할건지 등을 말이다.

웹서버냐, 파일 서버냐, 게임서버냐 따라서도 이것들의 선택을 잘해야한다.

보통 서버 모델이 같다면 그 구조는 크게 변하지 않는다. epoll서버를 보면 구조가 대부분 비슷하다.

epoll 뿐만 아니라 Select 기반으로된 서버의 기능적 모델을 보면 같은 모델끼리는 구조자체는 크게 다르지 않다.

여기서 시간이 꽤 지났으니 Select를 상기하고 가보자.

Tcp/ip 가 구현되고 사용할 수 있는 이유는 운영체제에 어느정도 표준화가 되어있기 때문이다.

select를 사용해도 운영체제의 도움을 받아서 처리를 하게된다.

하지만 select 자체는 운영체제에 의존적인 모델이 아니고 표준이 따로 존재하는 모델이다.

 

그래서 사용하기 불편할 수 있다. 왜냐면 파일 디스크립터에 대해 매번 처리해줬기 때문이다.

반대로 사용자는 불편했지만 운영체제한테는 무리를 주지는 않았다. 운영체제는 어떤게 처리되었다고 알려주기만 했기 때문이다.

운영체제가 알려주기만 하면 사용자가 따로 처리해줘야하기 때문에 Select는 간단하지만 번거로웠던 모델로 기억하면 된다.

select에서 2가지의 귀찮은 점이 존재했는데 select함수는 원본도 복사해야되었고 변화가 발생한 파일디스크립터를 또 확인해주는 번거로움이 있었다.

epoll은 이거를 해결했는데 select에서 불편했던 부분을 epoll은 원본복사도 해결하고, 파일디스크립터 체크도 해결했다는것.

위 문장을 통해서 epoll은 운영체제에게 분담시키는 모델인거를 추측할 수 있다.

즉 , epoll은 커널에 의존적이다. 이 말은 운영체제에 따라서 방법이 다르기 때문에 리눅스에만 존재한다 다른 운영체제는 다른 방법을 사용한다고 볼 수 있다.

 

Select기반의 IO 멀티플렉싱이 느린 이유


select를 보완한 epoll은 리눅스 계열에서 지원하는 모델이다.

- select 함수 호출이후 항상 등장하는 모든 파일 디스크립터를 대상으로하는 반복문.
- select함수를 호출할 때마다 인자로 매번 전달해야하는 관찰대상의 정보


Select 필요없는건가?


아래와 같은 경우 select도 좋은 선택지가 될 수 있다.


- 서버의 접속자 수가 많지 않다.
- 다양한 운영체제에서 운영이 가능해야한다.

운영체제 레벨이 아닌, 함수 레벨에서 완성되는 기능이다 보니, 호환성이 상대적으로 좋다. 

 

즉, 리눅스의 epoll 기반 서버를 윈도우의 IOCP기반으로 변경하는 것은 일이 될 수 있는데, 리눅스의 select 기반 서버를 윈도우의 select 기반 서버로 변경 하는것은 매우 간단하다.


epoll의 구현에 필요한 함수와 구조체


- epoll_create  : epoll 파일 디스크립터 저장소 생성
- epoll_ctl : 저장소에 파일 디스크립터 등록 및 삭제
- epoll_wait : select함수와 마찬가지로 파일 디스크립터의 변화를 대기한다.

wait함수에서 빠져 나올떄 변화가 발생한 파일디스크립터에 대한 정보를 반환해준다.

struct epoll_event
{
__uint32_t events;
epoll_data_t data;
}

typedef union epoll_data
{
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;



위의 세 함수 호출을 통해서 epoll의 기능이 완성된다.

위의 구조체는 소켓 디스크립터의 등록 및 이벤트 발생의 확인에 사용되는 구조체이다.


int epoll_create(int size);


성공시 epoll파일 디스크립터, 실패시 -1

운영체제가 관리하는 epoll 인스턴스라 불리는 파일 디스크립터의 저장소를 생성한다. 소멸시 close()함수를 통한 종료과정이 필요하다.
이 또한 파일 이기에.....


위의 함수호출을 통해서 생성된 epoll 인스턴스에 관찰대상을 저장 및 삭제하는 함수가 epoll_ctl이고, epoll 인스턴스에 등록된 파일 디스크립터를 대상으로 인벤트의 발생 유무를 확인하는 함수가 epoll_wait이다.



int epoll_ctl(int epfd, int op,  int fd, struct epoll_event p_event);


성공시 0 , 실패시 -1

epfd : 관찰대상을 등록할 epoll 인스턴스의 파일 디스크립터
op: 관찰대상의 추가, 삭제 또는 변경여부 지정
fd : 등록할 관찰대상의 파일 디스크립터
event : 관찰대상의 관찰 이벤트 유형.

두 번째 전달인자에 따라 등록, 삭제 및 변경이 이뤄진다.

- EPOLL_CTL_ADD : 파일 디스크립터를 epoll 인스턴스에 등록한다.
- EPOLL_CTL_DEL : 파일 디스크립터를 epoll 인스턴스에서 삭제한다.
- EPOLL_CTL_MOD : 등록된 파일 디스크립터의 이벤트 발생상황을 변경한다.

epoll_create 를 했을때 생기는 파일디스크립터를 epfd로 선택하고 거기서 어떤 fd를 컨트롤할 것인지 그리고 어떤 동작을 시킬건지 op 로 제어 한다.

 


epoll_ctl 함수 기반의 디스크립터 등록

 

struct epoll_event event;

event.events = EPOLLIN;
event.data.fd = sockfd;

epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd,&event);




- EPOLLIN : 수신할 데이터가 존재하는 상황
- EPOLLOUT : 출력버퍼가 비워져서 당장 데이터를 전송할 수 있는 상황
- EPOLLPRI : OOB 데이터가 수신된 상황
- EPOLLRDHUP: 연결종료되었거나 Half-close가 진행된 상황, 이는 엣지 트리거 방식에서 유용하게 사용됨.
- EPOLLERR : 에러 발생
- EPOLLLET : 이벤트의 감지를 엣지 트리거 방식으로 동작시킨다.
- EPOLLONESHOT : 이벤트가 한번 감지되면, 해당 파일 디스크립터에서는 더 이상 이벤트를 발생 시키지 않는다. 따랏 epoll_ctl 함수의 두 번째 인자로 EPOLL_CTL_MOD을 전달해서 이벤트를 재설정해야 한다.


비트 OR 연산으로 2개 이상 전달가능하다.

위 코드는 특정 소켓 파일디스크립터가 수신항 데이터가 존재하면 이벤트를 발생시키려고 운영체제에게 올린것이다.

그러면 저 소켓에게 데이터를 수신하면 이벤트가 발생할 것이다.

왜 이벤트에도 등록하고 epoll_ctl에도 등록을 해야하는지 궁금하다면 이유는 이벤트가 발생하면 인자로 전달한 event의 정보가 반환이 되기 때문이다.

그러면 event에 저장되어 있던 sockfd로 이벤트가 발생한 파일 디스크립터를 파악하게 된다.

epoll_ctl에서는 등록하는 목적으로 올려놓은것이고, event에는 파악하는 목적으로 sockfd를 등록한 것.


또한 epoll_event 구조체는 이벤트의 유형 등록에 사용되고 이벤트 발생시 발생된 이벤트의 정보로도 사용된다.


int epoll_wait(int epfd, struct epoll_event p_ events, int maxevents, int timeout);



epfd : 이벤트 발생의 관찰영역인 epoll 인스턴스의 파일 디스크립터
events : 이벤트가 발생한 파일 디스크립터가 채워질 버퍼의 주소값
maxevents: 두 번째 인자로 전달된 주소 값의 버퍼에 등록 가능한 최대 이벤트 수
timeout : 1 / 1000 초단위의 대기시간 , -1 전달 시 , 이벤트 발생까지 무기한 대기

int event_cnt;
struct epoll_event *ep_events;

ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);

event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);



epoll_wait 함수 반환 후, 이벤트 발생한 파일 디스크립터의 수가 반환되고, 두 번째 인자로 전달된 주소의 메모리 공간에 이벤트 발생한 파일 디스크립터 별도로 묶인다.

epoll_wait 함수의 두 번째 인자를 통해서 이벤트 발생한 디스크립터가 별도로 묶이므로, 전체 파일 디스크립터 대상의 반복문은 불필요 하게 된다.


위에서 malloc으로 할당한 이유는 이벤트가 발생한 소켓의 파일 디스크립터가 2개 이상이 될 수가 있기 때문에, 배열의 형태로 일단 공간을 마련해서 인자로 전달한다.

예를 들어서 두 개의 소켓이 있다고 가정하고 데이터를 수신이 되었다고 하면  한쪽에서 데이터를 보낼 때 우리가 저장한 구조체 변수가 각각 있을텐데 저기 malloc으로 인해서 배열에 각각 담긴다.

순차적으로 저장이되고 이벤트가 발생한 파일디스크립터가 2개면 epoll_wait에 반환값이 2이다.

여기서 반환값은 운영체제가 이벤트가 발생한 소켓의 정보를 담아서(미리 마련한 메모리 공간) 우리에게 전달. 거기서 몇개 인지도 알려준다.

여기서 select에서 나오지 않는 장점이 나오는 것이다.