[전산학] 리눅스 파일정보/ 링크(ln)/ 시스템콜/ 컴파일 과정

Hyo Kyun Lee·2021년 6월 26일
0

전산학

목록 보기
5/16

1. 리눅스 파일정보

리눅스의 기본은 파일입니다.

리눅스는 모든 device와 명령어, 실행과정을 모두 파일화하여 처리합니다.

그만큼 리눅스는 파일과 매우 밀접한 연관이 있는 체계이며, 관리체계의 시작도 파일정보의 관리부터 시작합니다.

사용자가 파일정보를 관리할 때 가장 기본이 되는 명령어와, 이와 관련하여 파일권한관리에 대해 알아보겠습니다.

ls

현재 디렉토리에서 어떤 파일이 있는지 파악하는 명령어는 ls 입니다.

ls 명령어를 옵션없이 사용한다면 아래와 같이, 현재 디렉토리에 있는 파일들을 확인할 수 있습니다.

현재 디렉토리내 하위 디렉토리도 같이 표기되며, 이를 통해 디렉토리도 하나의 파일형태로 관리되고 있음을 유추할 수 있습니다.

ls -al

현재 파일 및 디렉토리의 소유자 및 소유그룹, 기타사용자 및 각각의 권한정보까지 확인이 필요할 때 ls -al 명령어를 사용할 수 있습니다.
(*a옵션은 all, 기본적으로 숨겨진 파일도 모두 볼 수 있습니다)

아래와 같이 사용자정보/권한정보/파일크기/수정일자 등 파일에 대한 자세한 내역을 볼 수 있습니다.

파일권한

ls-al 명령어를 사용하면 각 행의 첫부분에 아래와 같은 권한정보가 나타나게 됩니다.

(a) 형태를 나타내며, -(파일/하드링크 파일) d(디렉토리) b(block device) c(character device) l(소프트링크 파일)을 나타냅니다.

(b) 소유자의 권한정보를 나타내며, rwx 권한이 모두 있습니다.

(c) 소유그룹의 권한정보를 나타내며, rx권한이 있고 w권한이 없습니다.

(d) 기타 사용자의 권한정보를 나타내며, r권한만 있고 wx권한이 없습니다.

파일형태의 경우, R/W/X 권한에 대한 동작은 아래와 같이 참조할 수 있습니다.

r(read) : 읽기 권한을 나타내며, 읽기만 가능하며 수정은 할 수 없습니다.
w(write) : 쓰기 권한을 나타내며, 읽기와 쓰기 모두 할 수 있습니다.
x(execute) : 실행 권한을 나타내며, 컴파일된 C파일이나 bash(쉘스크립트) 파일을 실행을 할 수 있습니다.

2. 링크(ln)

링크는 파일을 연결해주는 과정이며, 하드링크와 소프트링크가 있습니다.

  • 하드링크
    ln 명령은 파일을 링크할때 사용하며, 단독 사용시 하드링크(링크할 파일이 할당된 inode구조체주소를 똑같이 가리키는 파일을 똑같이 생성하며, 원본과 inode 번호 등 파일구성 및 주소가 완전히 동일한 파일이 만들어지는 과정)됩니다.

  • 소프트링크
    -s 옵션을 사용하면(ln -s) 소프트링크(링크할 파일의 주소를 가리키기만 하는 별도의 파일을 생성하며, 원본과 내용자체만 같고 inode 번호 및 파일용량 등 세부내역은 다른 파일이 만들어지는 과정)을 실행합니다.

코드예시1) 소프트링크, ln -s

아래의 예시는 링크명령중 소프트링크인 ln -s을 사용한 코드예시입니다.

$ ls
a.txt c.txt

$ cat a.txt
qwerty

$ ln -s a.txt b.txt
$ cat b.txt

소프트링크는 파일 자체만을 연결합니다.
출력결과 : qewrty

소프트링크 로직 수행과정

  1. ls 명령을 통해 현재 디렉토리에 어떤 파일이 존재하는지 보여줍니다.
    →a.txt와 c.txt 파일이 있습니다.

  2. cat 명령은 해당 파일의 내용을 모두 보여줍니다.
    →a.txt 파일은 qwerty 라는 내용이 작성되어 있습니다.

  3. ln -s original_file softlinked_file 명령은, original file(원본)을 소프트링크하여 softlinked_file을 생성합니다.
    → 해당 디렉토리에는 b.txt가 없는 상태이므로, b.txt가 만들어지면서 a.txt의 내용자체만 복사합니다.
    → cat 명령을 통해 복사한 내용인 qwerty를 화면에 출력하여 보여줍니다.

코드예시2) 하드링크, ln

아래의 예시는 링크명령중 하드링크인 ln을 사용한 코드예시입니다.

$ cat c.txt
asdfgh

$ ln c.txt d.txt

$ ls -ali
12608077 -rw-rw-r-- 1 fastcampus fastcampus 11  Nov  21 22:02 a.txt
[  (a)  ] lrwxrwxrwx 1 fastcampus fastcampus  5  Nov  21 22:02 b.txt -> a.txt
12608193 -rw-rw-r-- 2 fastcampus fastcampus  7  Nov  21 22:03 c.txt
[  (b)  ] -rw-rw-r-- 2 fastcampus fastcampus  7  Nov  21 22:03 d.txt

→ 현 디렉토리에서는 c.txt 파일이 존재하며, asdfgh 내용이 적혀져 있습니다.

c.txt를 하드링크한 d.txt 파일의 생성

c.txt를 원본으로 하는 링크파일이 ln 명령어를 통해 생성되었으며, 이 파일의 이름은 d.txt 입니다.

이 d.txt 파일은 하드링크된 파일이며, c.txt의 inode구조체주소를 그대로 참조합니다. 이에 따라 c.txt 파일의 inode 번호, 파일크기 등 모든 세부내역이 동일한 파일로 생성됩니다.

ls -ali를 통해 inode 번호까지 확인할 수 있습니다.

ls -ali 명령은 ls -al 명령시 확인가능한 정보, 즉 파일 소유자정보/권한정보 등 상세정보와 더불어 inode 번호까지 같이 확인해주는 명령입니다.

위 코드에서는 소프트링크 명령인 ln -s a.txt b.txt가 기재되어 있지않으나 b.txt 파일의 형태가 b.txt -> a.txt로 기재되어 있습니다. 즉 b.txt는 a.txt를 원본파일로 소프트 링크되어있는 파일입니다.

소프트링크 파일의 inode 번호는 원본 파일과 같지 않습니다.

ls -ali 명령을 통해 살펴볼 수 있듯이(b.txt -> a.txt), b.txt는 a.txt를 소프트 링크한 파일입니다.

소프트링크는 링크할 원본파일의 주소만 가리키며, 이외 파일크기 및 inode 주소 등이 다른 완전히 다른 파일로 생성되는 과정입니다.

따라서 소프트링크로 생성된 b.txt의 inode 번호(a)는 알 수 없습니다.

하드링크 파일의 inode 번호는 원본 파일과 동일하며, 원본 파일의 내용이 바뀌면 하드링크 파일의 내용도 같이 변경됩니다.

d.txt는 c.txt를 원본으로 하드링크된 파일입니다.

하드링크는 링크할 원본파일이 할당된 물리메모리 주소를 동일하게 가리키는 파일을 생성하며, 원본과 inode번호(12608193) 및 파일크기 등이 완전히 동일합니다.

참고로 원본인 c.txt의 내용이 바뀌면 d.txt도 바뀝니다.

코드예시3) 원본파일의 삭제 rm

$ rm a.txt c.txt

→ rm명령을 통해 a.txt와 c.txt 파일이 삭제된 상황입니다.

rm 명령을 통한 파일 삭제

rm options file1 file2 형태로 사용하는 rm명령어는, file1과 file2를 삭제합니다. 즉 위 조건에서는 a.txt와 c.txt 파일을 삭제하게 됩니다.

보통 함께 사용하는 option은 -rf로, 하위디렉토리까지 모든 내용을 삭제하는 -r 옵션과 삭제경고를 무시하고 진행하는 -f 옵션을 같이 사용합니다.

링크원본파일이 모두 삭제된 상황에서 접근가능한 파일은 하드링크 파일인
d.txt가 됩니다.

b.txt는 원본파일인 a.txt를 소프트 링크한 파일로, a.txt 파일의 주소만을 가리키는 “바로가기 파일”입니다.

따라서 a.txt가 삭제된 후 b.txt는 아무것도 없는 공간을 가리키며, 어떤 data도 없기때문에 어떤 내용도 없는 공파일 상태가 됩니다

정확히 말하면 vi b.txt를 통해 파일 접근이나 수정자체는 가능하지만, 원본인 a.txt가 삭제되었기때문에 a.txt의 내용을 다시 접근/확인할 수 없는 상태가 됩니다


d.txt는 c.txt를 원본파일로 하여 하드링크를 통해 생성된 파일입니다.

d.txt 파일은 c.txt 파일의 inode 구조체를 가리키는 파일로, 원본파일과 동일한 inode구조 및 파일정보 등을 가지게 됩니다.

이때 c.txt 원본이 삭제되어도, inode구조체는 메모리에 남아있기때문에 d.txt는 원본내용을 그대로 유지한 상태로 있게됩니다.

따라서 원본내용이 삭제되어도, 원본내용을 그대로 유지한채로 접근이 가능합니다.

아래 출력예시에서 b_hard.txt는 b.txt의 하드링크 파일입니다. 원본인 b.txt가 없어져도 inode 구조체는 남아있기때문에(=b_hard.txt) 원본데이터 및 파일이 그대로 유지되어, 하드링크된 파일도 이상없이 유지가 되는 것을 확인할 수 있습니다.

3. 시스템콜

리눅스의 시스템콜은 시스템 자원을 사용하기 위한 기초라 할 수 있습니다.

아래 명령어는 시스템콜을 호출할때 사용하는 코드예시이며, 코드예시를 통해 시스템콜에 대한 전반적인 과정과 개념을 이해해보겠습니다.

시스템콜을 이해하기위해선 먼저 아래 4가지 개념들을 먼저 이해해야 합니다.

A. 시스템콜 프로그래밍
시스템콜마다 사용하는 인자들과, 각 인자에 해당하는 의미가 다릅니다.

따라서 프로그래머들은 시스템콜 호출시 해당 시스템콜에 대한 인자가 어떤 것이 있으며 어떻게 활용해야할지를 먼저 판단해야합니다.

B. 어셈블리어
위 코드예시는 어셈블리어입니다.

어셈블리어는 컴퓨터가 연산 및 처리시 직접적으로 활용하는 기계어와 일대일로 대응되는 언어로, 인터페이스 계층으로 보았을때 사용자보다는 컴퓨터 측면에서 고안되어 저급언어라고도 합니다.

어셈블리어는 컴파일과 처리속도가 매우 빠르다는 장점이 있습니다.

C. 레지스터
레지스터는 기억회로인 플리플랩이 여러개 모여있는 하나의 하드웨어 장치로, 컴퓨터의 연산처리를 한 값이나 처리를 하기 위한 값을 임시로 “기억”하는 기억장치입니다.

컴퓨터, 즉 CPU는 자신이 처리하기위한 data를 메모리 할당을 통해 전달받습니다.

이 메모리 할당이 이루어지기전에 대부분은 기억장치에 먼저 해당 값들이 이동하고, 그 이후에 최종적으로 메모리 할당 및 연산처리가 이루어집니다.

시스템콜 처리시에도 마찬가지로, 위와 같이 레지스터를 통해 인자를 기억하고 시스템콜 처리시 해당 인자를 가져오는 과정을 진행하게 됩니다.

특정 개수를 넘어가는 인자들은 stack에 저장됩니다.

D. 레지스터를 통한 인자전달
위 시스템콜 프로그래밍에서 기술하였듯이, 프로그래머들은 시스템콜에 알맞는 인자들을 파악하고 이를 코드에 반영할 수 있어야 합니다.

인자전달방식도 레지스터방식, stack 방식 등 여러가지 방법이 있으므로 각 방법에 맞는 인자와 각각의 의미를 잘 살펴보고 코드에 반영할 수 있어야 합니다.

※시스템콜 관련 인자들을 정리한 사이트를 참고할 수 있습니다.
Chromium OS Docs - Linux System Call Table (googlesource.com)

※시스템콜 번호를 아래 디렉토리 경로를 통해 알 수 있습니다.
vim /usr/include/sys/syscall.h

위 코드에서 시스템콜을 사용할 때 활용하는 레지스터는 아래 4항목입니다.

eax // 시스템콜 번호를 나타냅니다.

직접값 모드를 통해 레지스터에 값을 할당해주었습니다. 즉 eax레지스터에 4를 할당하여, 0x80에 있는 시스템콜들중 4번에 해당하는 시스템콜을 사용하도록 명령합니다.

ebx // 시스템콜 인자를 나타냅니다(write() 시스템콜의 fd인자).

메모리 직/간접주소모드 통해 레지스터에 값을 할당해준 것으로 보여집니다.

ebx레지스터에 3을 할당하여, 해당 주소가 fd(file descriptor) 인자로 전달됩니다.

향후 시스템콜이 실행될 때 실제적으로 data를 가져와 읽는 곳은 file descriptor이며, 주소값이 전달된 상태이므로 해당 주소에 있는 data를 전달받아 참조합니다.

ecx 시스템콜 인자를 나타냅니다(wirte() 시스템콜의 &buf인자).

직접값 모드를 통해 buf객체를 인자로 전달한 것으로 보여집니다.

즉, ecx레지스터에 buf 인스턴스를 전달하여, file descriptor에 실제 data가 담기기전에 임시적으로 data를 저장할 공간을 전달합니다.

이 원리는 앞서 기술한 레지스터의 원리와 유사합니다.

edx // 시스템콜 인자를 나타냅니다(wirte() 시스템콜의 count인자).

직접값 모드를 통해 얼마만큼의 크기를 시스템콜을 통해 읽을 것인지에 대한 인자를 전달하였습니다.

즉 edx레지스터에 14를 할당하여, 향후 시스템이 실행될 때 fd에 있는 data를 14크기만큼 읽게됩니다.

시스템콜 호출시 커널모드로 호출되며, 사용자모드에서 커널모드 변환시 인터럽트가 발생되어야 합니다.

int : 플리플랩에서 인터럽트를 발생시키는 OPcode 입니다.

즉 int 명령을 통해 인터럽트를 발생시키게 됩니다.

0x80 : 메모리 직접주소모드를 통해 시스템콜 관련 함수들이 모여있는 메모리 0x80주소로 이동합니다.

eax 인자를 참조하여 해당 시스템콜을 찾고 실행합니다.

위 개념들을 연계하여 시스템콜 코드 수행로직을 이해해보겠습니다.

기본적으로 컴퓨터는 작성한 명령을 순차적으로 실행합니다.

쉽게말하면 mov는 data 이동 및 해당 data를 가리키기위한 주소이동을 해주는 명령이며, 시스템콜을 사용하기위한 인자값을 차례대로 해당 레지스터에 할당시켜주는 과정이라 생각할 수 있습니다

할당하는 과정은 직접값방식, 직접주소방식, 간접주소방식 등이 있는데 상황에 따라 효율적인 방법을 사용해야 합니다.

레지스터에 각 인자값을 할당하여 시스템콜을 사용할 준비가 된 이후에,
시스템콜을 사용하기위한 인자들은 인자할당방식(레지스터 전달 및 stack 전달 등)에 따라 차례대로 전달됩니다.

범용레지스터가 인자전달시 활용되는 공간입니다.

레지스터 인자들은 eax, ebx, ecx, edx에 저장되며, 이 레지스터들은 모두 범용 레지스터의 일종입니다.

범용레지스터는 작업레지스터에서 data 처리 및 연산이 용이하도록 임시로 자료를 저장하는 공간을 일컫습니다.

레지스터 할당 > 시스템콜 호출 > 래퍼함수의 인자전달 및 명령실행 > 후처리

  1. 레지스터 할당
    mov eax, 0x04 : 범용레지스터인 eax에 직접값모드를 통해 4가 전달되어 기억됩니다.
    mov ebx, 0x03 : 범용레지스터인 ebx에 직/간접주소모드를 통해 3이 전달되어 기억됩니다.
    mov ecx, &buf : 범용레지스터인 ecx에 직접값모드를 통해 buf포인터(인스턴스)가 전달되어 기억됩니다.
    mov edx, 14 : 범용레지스터인 edx에 직접값모드를 통해 14가 전달되어 기억됩니다.

  2. 시스템콜 호출
    int 0x80 : 인터럽트를 발생시켜 CPU는 커널모드로 전환, 0x80 주소로 접근하여 시스템콜 함수를 호출합니다.

  3. 인자전달 및 명령실행
    시스템콜이 호출된 후 래퍼함수가 시스템콜의 인자를 전달받고, 커널모드내 특정 레지스터에 저장합니다. 최초 인자로 시스템콜번호(eax)가 전달되어, 해당 시스템콜 번호가 매핑된 명령을 실행합니다. 위 조건에 따르면 write() 시스템콜이 호출됩니다. 시스템콜 서비스 루틴은 이처럼 인자들을 확인하면서 각 인자에 맞는 작업들을 실행하는데, 위 인자들에 따라 14만큼의 크기를 가진 data를 buf에 임시저장하며, 이 buf에서 기억하고 있는 내용을 fd(3번주소에 있는 공간)에 할당하여 최종적으로 내용을 write(), 즉 입력하게 됩니다.

  4. 최종상태값 반환 및 후처리
    서비스 루틴 함수는 시스템콜 처리가 완료된 상태를 system_call() 루틴에 반환하며, 커널스택에서 레지스터 값들을 복원하고 시스템콜 호출 반환값을 전달받습니다. 이후 사용자모드로 전환하면서 기존 실행하였던 프로세스를 처리합니다.

4. 컴파일 과정

사용자가 프로그래밍한 명령어를 기계가 이해할 수 있도록 컴파일합니다.

리눅스의 gcc 명령은 GNU Compiler Collection의 약자로, 말 그대로 파일을 컴파일하고 실행하기위해 필요한 일련의 작업을 하는 명령입니다.

이는 리눅스 환경에서 파일 컴파일을 하기위해선 gcc패키지를 설치가 필요하다는 의미와 같습니다.

이후 gcc 명령 실행시 전처리기, 어셈블러, 링커, C컴파일러를 실행하며 파일을 실행시키기위한 준비작업을 하게 됩니다.

컴파일 과정은 4단계로 나뉩니다.

아래 C파일의 소스코드가 컴파일 된다고 가정해보겠습니다.

  1. 전처리기 test.c 파일을 gcc 명령시 최초 전처리기(cpp)가 작동합니다.
    #include <stdio.h> 등과 같이 주로 헤더파일을 전처리하는 명령을 통해 전처리기가 작동되며, 헤더파일을 전처리(확장)하여 헤더파일의 구조체 및 여러 변수들을 소스코드내에서 간편하게 사용할 수 있게 도와주는 역할을 합니다.

    → 소스파일이 전처리기를 거치면 최종적으로 test.i 파일이 생성됩니다.

  2. C컴파일러 test.i 파일이 생성된 후, 어셈블리어로 된 file.s 파일이 생성됩니다.

    → 어셈블리어는 기계가 이해할 수 있는 기계어와 1:1 대응합니다.

  3. 어셈블러 어셈블리어 파일인 file.s를 기계가 직접 이해할 수 있는 기계어로 번역하며, 오브젝트 파일을 생성합니다.

    → 오브젝트 파일은 test.o 입니다.

  4. 링커 기계어로만 이루어진 오브젝트 파일은 해당 함수를 이해할 수 있는 라이브러리가 없기 때문에 독립적으로 실행할 수 없습니다.
    이때 라이브러리를 연결해주거나, 프로그램의 경우 파일간 연결관계를 형성하여 실행이 가능하도록 연결관계를 만들어주게 됩니다.
    이 과정을 링크단계라 하며, 최종적으로는 실행파일을 형성하는 단계입니다.

    → 최종적으로 컴파일이 완료된 코드는 바이너리 코드의 형태이며, gcc option을 별도 설정하지 않았다면 test.out 실행파일이 생성됩니다.
    → 보통은 gcc -o test 등으로 별도 실행파일의 이름을 지정해주어 실행파일을 생성하도록 설정합니다.

0개의 댓글