목차
이 책을 읽은 이유
들어가기 전에
프로그램, 프로세스 차이
커널
커널의 부팅 과정
시스템 콜
시스템 콜의 매개변수는 레지스터다
정적 링크 vs 동적 링크
어떻게 공유 라이브러리를 찾아갈까?
Reference
이 책을 읽은 이유
예전에 보안을 공부하고, C로 네이티브 개발을 하다 보니 커널 코드를 분석해 보고 싶었다.
하지만, 무작정 커널 분석을 시작하기엔 막연한 부분도 많고, 기억 안 나는 부분도 있어 개요를 잡을 필요가 있었다.
스터디에서 선정된 책이지만, 개인적으로 괜찮다고 생각해 스터디를 신청하고 읽게 됐다.
들어가기 전에
책을 읽기 전에 OS의 정의에 대해 정리할 필요가 있었다.
OS ⇒ 유저와 하드웨어를 연결하는 인터페이스
인터페이스의 의미가 “서로 이어 준다”는 뜻이 있어, 브리지(bridge)라고 표현한 글들도 있었다.
프로그램, 프로세스 차이
이름이 비슷하기 때문에 섞어서 말하는 케이스도 있고, 잘못 말해도 알아서 이해하고 넘어간다.
하지만 책을 보고 정리하기 때문에 용어 정리는 잡아야 한다 생각한다.
프로세스⇒ 실행 중인 프로그램 (동적)
프로그램⇒ 실행 파일 (정적)
C 같은 컴파일러형 언어는 소스를 빌드 해서 나온 실행 파일이 프로그램이고
파이썬 같은 스크립트 언어는 소스가 프로그램이 된다.
커널
OS의 핵심 기술이며, 소프트웨어와 하드웨어를 연결하는 인터페이스
프로세스가 하드웨어에 접근하기 위해선 커널에 정의된 시스템 콜을 호출해야 한다.
커널을 거쳐가는 이유는 다음과 같다.
1. 악의적인 or 의도치 않은 하드웨어 접근을 막기 위해
2. 역할 분리(개발자는 하드웨어를 몰라도 커널을 통해 조작이 가능하게)
3. 효율적인 하드웨어 자원 사용
시스템 콜을 요청하게 되면 커널 모드로 전환이 필요하다.
이때 예외 이벤트를 발생시켜 CPU에 해당 프로세스가 커널 모드로 전환한다는 것을 알린다.
작업이 끝나면 다시 CPU에 예외 이벤트를 발생시키고
CPU는 프로세스를 사용자 모드로 전환시켜 명령을 마저 수행할 수 있도록 한다.
예외 이벤트에 대해선 4장에서 자세하게 다룬다.
커널의 부팅 과정
책에는 없는 내용이지만, 커널이란 프로그램을 누가 부팅 시키는지 궁금했다.
검색 결과 부트로더가 그 작업을 담당하고 부팅된 커널은 OS에 필요한 프로세스들을 생성한다.
시스템 콜
우리가 사용하는 라이브러리들의 내부를 보면 여러 개의 시스템 콜로 구성돼 있다.
strace 명령어를 통해 어떤 시스템 콜이 호출됐는지 알 수 있다.
또한 sar 명령어를 통해 CPU가 시스템 콜 처리에 어느 정도 비율을 사용했는지 알 수 있다.
다음 명령어를 실행하고 %system 영역을 보면 수치가 오른 것을 확인할 수 있다.
#include <unistd.h>
int main() {
while (1) {
getpid(); // 현재 프로세스의 PID를 가져옵니다.
}
return 0;
}
시스템 콜의 매개변수는 레지스터다
시스템 콜들의 매개변수는 대부분 4개 이하고, 6개를 넘는 것은 없다.
그 이유는 intel 기준 32bit 환경은 매개변수 4개까지 레지스터로 처리하고
64bit 환경은 6개까지 레지스터로 처리한다.
그래서 우리가 사용하는 함수도 시스템 콜처럼 매개변수를 먼저 레지스터에 저장하고
부족하다면 메모리에 저장한다.
아래 코드의 myFunction 함수를 어셈블리어로 표현한 결과다.
// myFunction(1, 2, 3, 4, 5, 6, 7, 8);
void myFunction(int a, int b, int c, int d, int e, int f, int g, int h) {
printf("전달받은 매개변수들:\\n");
printf("a: %d\\n", a);
printf("b: %d\\n", b);
printf("c: %d\\n", c);
printf("d: %d\\n", d);
printf("e: %d\\n", e);
printf("f: %d\\n", f);
printf("g: %d\\n", g);
printf("h: %d\\n", h);
}
어셈블리를 보면 1~6까진 레지스터에 저장하고 7, 8은 메모리에 저장한다.
도식화하면 다음과 같다.
정적 링크 vs 동적 링크
과거에서부터 동적 링크를 주로 사용해온 이유는 다음과 같다.
- 프로그램별로 같은 라이브러리를 메모리에 로드하는 건 자원 낭비
- 자주 사용되지 않는 라이브러리를 처음부터 메모리에 로드할 필요 X
하지만 이런 과정은 치명적인 문제가 있는데, 프로그램이 해당 OS에 종속된다.
그래서 잘 돌아가는 프로그램이 다른 환경에서 잘 작동한단 보장이 없다.
그래서 최근에 개발된 Go 언어는 정적 링크를 통해 이식성을 확보했다.
위에서 정적 링크의 비효율성을 언급했음에도 이 기술을 적용한 이유는 다음과 같다
1. 대용량 메모리와 저장 장치가 널리 사용
2. 실행만 된다면 다른 환경에서도 동작
3. 공유 라이브러리를 찾는 시간 단축해 시작 시간 빠름
4. 라이브러리를 공유하는 다른 프로세스 간 호환성 문제 해결
어떻게 공유 라이브러리를 찾아갈까?
PLT & GOT를 활용해 공유 라이브러리를 동적으로 연결할 수 있는 정보를 제공한다.
PLT (Procedure Linkage Table) : 공유 라이브러리를 호출할 때 처음 도달하는 곳. 호출할 함수의 진입 정보를 갖고 있다.
GOT (Global Offset Table) : PLT가 참조하는 테이블. 실제 함수가 어느 메모리에 로드됐는지에 대한 정보를 갖고있다.
위의 예시 코드에 MyFunction을 어셈블리로 보면 put, printf 호출 코드에 plt가 붙은 것을 알 수 있다.
여기서 puts@plt로 들어가 본다.
나도 어셈블리 모르니 걱정하지 않아도 된다.
노란색 친 부분 puts@plt+4를 보면 got.plt란 곳으로 이동하라고 한다.
got.plt는 실제 주소에 함수를 설정하는 동안 함수 호출을 지연시키는 용도이다.
첫 호출에만 got.plt가 쓰이고 이후엔 got가 호출된다.
지금은 0x3fc8, 0x3fd0에 의미 없는 값이 있지만, got가 함수 offset을 알게 되면 해당 함수를 호출한다.
복잡하지만 흐름 구성을 도식화한 결과다.
Reference
https://www.geeksforgeeks.org/how-linux-kernel-boots/
https://blog.bytebytego.com/p/ep88-linux-boot-process-explained
https://news.hada.io/topic?id=15239
https://www.geeksforgeeks.org/difference-between-interrupt-and-exception/
https://stackoverflow.com/questions/125394/interrupts-and-exceptions
'개발' 카테고리의 다른 글
그림으로 배우는 리눅스 구조 2주차 (0) | 2025.03.29 |
---|---|
[JPA] FindAll이 같은 값만 나와요 (2) | 2025.02.15 |
[PostgreSQL] FAQ를 번역해 보았다. (스압) (2) | 2025.01.29 |
[PostgreSQL] FDW로 Cross Database 해결하기 (0) | 2025.01.19 |
파일 옮길 땐 tar를 쓰자 (0) | 2024.11.19 |