본문 바로가기
Linux/Linux 이해

3. Linux(리눅스) 이해 - 프로그램 생명주기 단계 program lifecycle phase / runtime 런타임

by ahsung 2023. 3. 27.

프로그램은 하드웨어를 실행할 수 있는 일련의 코드 나열이며, 이런 프로그램을 어떻게 관리하고 어떤 환경에서 실행시킬지는 하드웨어에 따라 운영체제에 따라 달라질 수 있습니다.

 

위키피디아에서는 이런 개념을 Program lifecycle phase라고 소개하고 있고 현대 OS는 대부분 위키피디아의 내용과 유사한 구성을 이루고 있습니다. Computer Science와 흔히 it계열에서 말하는 runtime도 phase에 들어가는 개념입니다.

(요즘 여러 상황에서 혼용되어 사용하지만 전반적인 의미는 유사함)

https://en.wikipedia.org/wiki/Program_lifecycle_phase

 

Program lifecycle phase - Wikipedia

From Wikipedia, the free encyclopedia Program lifecycle phases are the stages a computer program undergoes, from initial creation to deployment and execution. The phases are edit time, compile time, link time, distribution time, installation time, load tim

en.wikipedia.org

 

Phase

딱히 linux의 동작을 이해하는데 중요하지 않은 phase는 제외했습니다.

 

1. Compile time

고급 프로그래밍 언어를 실제로 CPU가 이해할 수 있는 기계어/binary_code로 변환되는 과정입니다.

프로그래밍언어마다 해당 작업을 이행해줄 컴파일러가 제공될 수 있습니다.

컴파일러가 없다면, 모든 프로그래밍은 CPU가 이해할 수 있는 저수준의 어셈블리 언어로 코딩될 필요가 있습니다.

어셈블리 언어는 CPU가 직접적으로 이해하는 기계어와 1 대 1로 대응되는 니모닉 기호(mnemonic symbol)로 구성된 아주 단순한 프로그래밍 언어입니다.  어셈블리어 -> 기계어 번역은 어셈블러라는 프로그램으로 수행할 수 있으며, 기계어와 1 대 1 대응이기 때문에 매우 단순하게 설계될 수 있습니다.

 

https://en.wikipedia.org/wiki/Compile_time

 

Compile time - Wikipedia

From Wikipedia, the free encyclopedia The time during which source code is validated and converted to machine code by a compiler In computer science, compile time (or compile-time) describes the time window during which a language's statements are converte

en.wikipedia.org

 

2. Link time

링크 타임은 기계어로 컴파일된 코드 묶음인 object file 여러 개를 서로 연결하는 시간입니다.

보통의 경우, 프로그램은 여러개의 컴파일된 파일들을 재사용할 수 있고 흔히 이런 재사용을 위한 코드 묶음들을 library라고 부릅니다.

각 라이브러리의 함수, 변수등을 사용하기 위해서는 서로의 위치를 찾을 수 있도록 코드들의 배치가 정해져야 하고 이 과정을 link라고 부릅니다.

main 함수에서 printf()를 사용한 경우, printf()는 "stdio.h"라는 라이브러리에 포함되어있지만 어떻게 호출해야 될지는 link 과정에서 정해집니다.

ex) 프로세스의 가장 첫번째 주소 + 1024byte부터가 printf 함수의 서브루틴 시작점이다.

 

https://en.wikipedia.org/wiki/Link_time

 

Link time - Wikipedia

From Wikipedia, the free encyclopedia -->

en.wikipedia.org

 

3. Load time

link 과정은 한 개의 프로스세스 메모리 내부에서 일어나는 규칙입니다.

가상 메모리를 사용하는 OS에서 프로세스 스스로는 전체 메모리 주소를 사용하는 것처럼 인식하기 때문에 link때 정해진 메모리 규칙만으로 동작할 수 있으나, 운영체제가 여러 개의 프로세스를 관리하기 위해서는 물리 메모리에 적당히 구분되어 load 되어야 합니다.

loader는 링크까지 완료된 바이너리 코드들을 실제 물리 메모리에 매핑하여 올리는 작업을 진행합니다.

 

> 링크 타임에는 가상 메모리의 주소상 상대적 위치만 결정, Load 타임에는 가상메모리와 물리적 메모리의 매핑이 진행됨

 

https://en.wikipedia.org/wiki/Loader_(computing) 

 

Loader (computing) - Wikipedia

From Wikipedia, the free encyclopedia Part of an operating system For applications that speed up loading from a tape or disk drive, see fast loader. In computer systems a loader is the part of an operating system that is responsible for loading programs an

en.wikipedia.org

 

* Dynamic linker

프로세스가 처음 부팅될 때, 운영체제는 로더를 통해서 프로그램을 메모리에 로드하고 과정에서 프로그램에는 shared library 등을 동적으로 필요로 하다는 정보가 포함될 수 있습니다.

이 경우 OS는 dynamic linker를 호출하여 프로세스 부팅타임에 라이브러리를 동적으로 링킹 합니다. (재배치)

당연히 링킹 하는 과정에서 추가적인 library load가 발생합니다. 

 

좀 더 자세한 과정은 아래 포스팅 참고 

https://asung123456.tistory.com/18

 

Linux C/C++ shared library 컴파일하기 fPIC 옵션 GOT, PLT

c++ g++ -shared -fPIC -o g++ -shared fPIC -o lib.so code.cpp c gcc -shared [-fPIC] -o gcc -shared [-fPIC] -o lib.so code.c -fPIC 생략가능 하지만 공유라이브러리를 만들 때는 -fPIC 옵션 사용을 권장합니다. (컴파일 종류에 따

asung123456.tistory.com

 

 

4. Run time

프로세스로서 실질적인 바이너리코드가 CPU에 실행되고 있는 단계입니다.

runtime이라는 말은 여러 it 개념에서도 다양하게 혼용되어 사용하고 있지만, 일반적으로 CPU에 기계어가 실행되어 실제로 하드웨어가 동작중인 시간입니다.

runtime 중에 발생하는 에러는 운영체제에 의해서 인터럽트 될 수 있습니다.

https://en.wikipedia.org/wiki/Runtime_(program_lifecycle_phase) 

 

Runtime (program lifecycle phase) - Wikipedia

From Wikipedia, the free encyclopedia Period during which a computer program is running (executing) This article is about the execution phase in a programming language. For the infrastructure required to run programs, see Runtime system. In computer scienc

en.wikipedia.org

 

 

 

Runtime과 운영체제

흔히 it에서 runtime은 runtime system, runtime environment등과 혼용되어서 사용됩니다.

딱히 문제는 없지만 모두 실제로 기계어로 번역되어 실행되고 있는 환경, 시간, 플랫폼이라고 이해할 수 있습니다.

 

예로 javascript는 javascript문법으로 코딩된 스크립트들이 있고, 실제로 nodejs, chrome(브라우저)등 스크립트를 실행시키는 runtime이 있습니다.

 

그런 의미에서 운영체제는 모든 프로그램의 가장 기본적인 runtime 개념을 제공하고 있습니다.

linux의 경우, 바로 c runtime library가 대표적입니다.

javascript나 python, jvm과 같이 특정 소프트웨어가 대신 처리해 주는 runtime 환경과는 다르지만, C runtime(CRT) library는 C언어 프로그래밍되는 언어들에게 runtime에 동작해야 될 기본적인 함수들과 로직들을 제공해 줍니다.

CRT 라이브러리는 흔히 C 표준라이브러리의 구현체라고 볼 수 있습니다.

즉,  printf(), scanf(), write(), read()등의 함수를 내부적으로 구현한 로직이며 이것은 운영체제마다, 컴파일러마다 다를 수 있습니다.

C 표준라이브러리는 C 프로그래밍을 하는 모든 사람에게 표준 인터페이스를 제공하기 위한 일종의 약속(ISO C)이지만, 운영체제마다 runtime 동작 방식은 다를 수 있습니다.  그렇기 때문에  C 표준라이브러리의 구현체인 C runtime library는 OS마다 컴파일마다 다르게 사용할 수 있으며, linux 계열에서는 대표적인 C runtime library인 glibc를 사용하고 있습니다.

 

C runtime library의 함수를 끝까지 추적하면 그 끝은 systemcall을 통해 커널의 기능을 호출하고 있으며, c runtime libraray는 커널의 동작을 wrapping 하여 추상화한 계층입니다.

 

보통 C언어는 컴파일언어이기 때문에 보통 코딩/컴파일 과정에서 참조되는 library를 왜 runtime이라고 하는지 의문일 수 있습니다.

runtime library는 보통 공유, 동적 라이브러리입니다. static 컴파일을하게되면 운영체제에 설치된 glibc 버전이나 채택된 표준라이브러리에 따라 실행파일에 기계어 코드가 직접 작성되지만, static 옵션이 없는 일반적인 컴파일인 경우 라이브러리를 동적 링킹한다는 정보만 있을 뿐 실제 CRT 라이브러리 코드가 컴파일되어서 실행파일에 들어가지 않습니다.

예로, printf() 하나를 사용한 c언어를 컴파일해도 내부 기계어를 뜯어보면 printf 구현 로직은 보지이 않을 것입니다.

그럼 OS는 printf를 어떻게 실행시키나? -> 실행파일이 실행되는 순간. 동적링킹 섹션을 찾아, OS에 설정된 C Runtime Library를 동적으로 링킹&로딩하여 사용합니다.

 

C언어는 컴파일언어이기 때문에 한 번 컴파일된 프로그램은 같은 linux 커널에서 모두 동일하게 동작될 것 같지만,

사실 OS에 설치된 CRT 라이브러리마다 조금씩은 다르게 실행될 수도 있고 실패할 수도 있습니다.

대표적으로 yum, apt로 몇몇 툴들을 설치할 때 *-devel등의 확장판 개념의 라이브러리들이 의존성으로 연결되어 추가 설치되는 경우를 종종 볼 수 있습니다.

 

정리하면, 일반적인 c 컴파일러는 공유라이브러리의 동적링킹 정보만 입력되고, 실제 실행되는 순간 OS의 Rumtime Library를 동적으로 로딩하고 의존성을 갖습니다.

반면, static 컴파일을하게되면 컴파일할 당시의 OS의 Runtime Library 코드를 컴파일하여 실행파일에 정적으로 넣고 실행합니다.

이런 static 컴파일된 바이너리(실행파일)는 OS마다 다를수 있는 runtime library의 의존성은 없지만, 오히려 컴파일했었던 OS의 runtime library와 컴파일러등의 구현 의존성이 있습니다.

-> 간단한 예시입니다. 실제가 아닙니다.)

-> Ubuntu22의 CRT로 static 컴파일한 바이너리를 ubuntu 14에서 실행.

-> C표준라이브러리의 간단한 open() 함수를 사용하는 로직

-> Ubuntu22의 CRT와 ubuntu14의 CRT는 open()을 구현할 때 사용한 시스템콜이 다름 (커널버전과 glibc등의 버전에 따른 차이)

-> 결과적으로 ubuntu14에서 바이너리가 실패

 

즉, static 컴파일이 코드를 담을 수 있다고 좋은 것은 아닙니다. (+ static 컴파일하게되면 코드의 양이 상당히 커지기 때문에 프로세스 부티타임 자체가 길어지고 프로세스마다 거의 동일하게 사용하는 함수들을 메모리 코드영역에 중복해서 담아야함)

CRT library는 시스템콜을 직접 호출하는등 C표준라이브러리의 구현체이기 때문에 커널과 굉장히 밀접하고, 오히려 커널이 다른 시스템간에서는 구현에 차이가 발생하 호환성이 좋지 않을 수 있습니다.

 

이럴 때는 오히려, 잘 변하지 않는 일종의 인터페이스 역할인 C표준라이브러리를 기준으로 코딩/컴파일하고, 실제 실행되는 OS의 CRT를 동적으로 로딩하는 것이 더 좋을 수도 있습니다.

 

반대로, POSIX를 따르거나 runtime library가 제대로 존재하지 않을 것 같은 OS에 배포가 필요하다면 static이 유리할 수 있습니다.

(물론 위에서 언급했던 우려대로 POSIX 시스템콜 호환성과 바이너리파일 포맷 동일성등이 보장되어야합니다.)

 

 

참고 설명

https://github.com/0xAX/linux-insides/blob/master/SysCall/linux-syscall-1.md

 

GitHub - 0xAX/linux-insides: A little bit about a linux kernel

A little bit about a linux kernel. Contribute to 0xAX/linux-insides development by creating an account on GitHub.

github.com

 

대표적으로 write() 함수 하나를 추적하면, (아래는 실제로 존재하지 않는 pesudo 코드입니다)

-> write()함수를 사용하여 컴파일된 바이너리 파일 실행

-> 커널은 바이너리파일의 동적링킹 섹션을 읽어들임

-> OS의 공유라이브러리를 동적으로 링킹&로딩(가상메모리 매핑)

->  write의 구현부 -> 비즈니스로직 실행(인자 정리 등등) -> 시스템콜 SYSTEMCALL("write", arg1, arg2 ....) 

-> 시스템콜 부분은 보통 어셈블리언어로 CPU의 trap 명령어를 사용하여 커널모드로 전환

-> 커널 메모리 어딘가에 등록된 write 시스템콜 서브루틴 실행

-> vfs_write() -> 현재 파일시스템 추적 -> xfs라면, -> xfs_write()  # 커널 내부에 모놀로틱하게 이미 구현/컴파일된 코드영역 실행

(참고로 커널의 모든 함수/서브루틴은 OS가 부팅되는 시점에 항상 커널 메모리 영역에서 상주하고 있습니다.)

 

 

결론

linux 커널이 C언어로 개발되었고, 그에 대한 runtime을 위한 환경들도 C언어 기반으로 작성되었습니다.

linux 커널 기능을 가장 쉽게 접근할 방법은 C runtime library를 사용하는 것이기 때문에, 결국 python, jvm, nodejs와 같은 고급언어의 runtime들의 저수준 부분은 C언어(C++포함)로 개발된 경우가 대부분입니다. 

 

linux 플랫폼 위에서 작동하는 모든 프로그램들은 결국 대부분이 저수준 영역에서 C Runtime library을 사용하기 때문에,

JVM, Python 등이 OS에 종속성 없이 동작한다고 하지만 이는 고급언어 수준의 코드가 각 OS마다 통일되게 사용할 수 있을 뿐

각 OS마다 JVM, python 등의 runtime은 운영체제에 맞게 개발되며, 현재 운영체제 환경에 종속적입니다.

예로 JVM에서 사용하는 특정 함수가 특정 OS가 사용하는 c runtime library 버전과 호환되지 않거나 c runtime library와 linux 커널 버전의 호환성이 맞지 않아 버그가 발생할 수 있습니다.

 

즉, linux 플랫폼에 기반하는 대부분의 프로그램들은 어떤 고급언어를 사용한다고 한들, 위에서 설명한 program lifecycle phase에 종속적인 관계를 갖습니다.

 

물론, 최근에 유행하는 Go와 같은 컴파일언어는 직접 kernel systemcall 부분을 구현한 Go runtime Library를 갖고 이를 C언어와 무관하게 컴파일이 가능하기도 합니다. (물론 컴파일된 이진코드가 linux 운영체제가 이해할 수 있는 방식이어야 합니다. 예로 ELF)

 

물론, Go언어가 완벽하게 C runtime library와 독립적이진 않고 일부 Go 라이브러리 중 매우 복작한 기능들(네트워킹, 암호화)등은 아직 일부 C 언어로 컴파일된 라이브러리를 사용하고 있습니다.

 

어쨌든 Go 언어가 C와는 독립적일지라도 linux위에서 동작하는 방식은 다르지 않습니다.

 

 

댓글