리눅스 aulast 명령어: 감사 로그로 사용자 접속 기록 완벽 분석
![]() |
alias인가, as인가? 리눅스 명령어의 두 얼굴
리눅스 터미널에서 'as'라는 키워드를 접했을 때, 많은 개발자와 시스템 엔지니어는 두 가지 다른 명령어를 떠올릴 수 있습니다. 하나는 셸에서 긴 명령어를 짧게 줄여주는 alias
이고, 다른 하나는 소프트웨어 개발의 가장 근본적인 단계에 관여하는 as
, 즉 GNU 어셈블러입니다.
alias
는 셸(Shell)의 내장 명령어로, 복잡하고 긴 명령어를 자신만의 짧은 별칭으로 만들어 작업 효율을 높여주는 편리한 도구입니다. 예를 들어, ls -alF
라는 자주 사용하는 명령어를 ll
이라는 별칭으로 만들고 싶다면 alias ll='ls -alF'
와 같이 설정할 수 있습니다. 이러한 설정은 보통 사용자의 홈 디렉터리에 있는 .bashrc
파일에 저장하여 셸이 시작될 때마다 자동으로 적용되게 합니다.
하지만 이 글에서 집중적으로 다룰 대상은 alias가 아닌, 훨씬 더 근본적이고 강력한 도구인 as
(the GNU Assembler) 입니다. as
는 GNU 툴체인의 기본 어셈블러로서, 컴파일러가 생성한 인간이 읽을 수 있는 어셈블리 코드를 컴퓨터가 직접 실행할 수 있는 기계어(오브젝트 코드)로 변환하는 핵심적인 역할을 수행합니다.
본 문서는 as
가 컴파일 과정에서 어떤 역할을 하는지부터 시작하여, 복잡하게 느껴질 수 있는 문법의 차이, 실제 어셈블리 프로그램을 작성하고 실행하는 방법, 그리고 매크로 프로그래밍과 크로스 컴파일 같은 고급 기법까지 심도 있게 탐구할 것입니다. 시스템 엔지니어부터 IT 분야에 입문하는 학생과 개발자까지, 모두에게 유용한 지침서가 되는 것을 목표로 합니다.
컴파일의 여정: 소스 코드에서 실행 파일까지
as
의 역할을 제대로 이해하기 위해서는 먼저 C와 같은 고급 언어로 작성된 소스 코드가 어떻게 실행 가능한 파일로 변환되는지, 그 전체적인 여정을 알아야 합니다. 이 과정은 크게 네 단계로 나눌 수 있으며, as
는 이 중 세 번째 단계를 책임지는 핵심 주자입니다.
1. 전처리 (Preprocessing)
컴파일의 첫 단계는 전처리기(cpp)가 담당합니다. 전처리기는 소스 코드에 포함된 #include
, #define
과 같은 전처리기 지시문을 처리합니다.
#include
는 명시된 헤더 파일의 내용을 소스 코드에 그대로 삽입하고, #define
은 정의된 매크로를 해당 값으로 치환합니다. 이 과정을 거치면 여러 파일과 매크로로 나뉘어 있던 코드가 하나의 순수한 C 소스 파일(보통 .i
확장자)로 통합됩니다.
2. 컴파일 (Compilation)
다음으로, 컴파일러의 본체(GCC의 경우 cc1
)가 전처리된 .i
파일을 입력받아 아키텍처에 종속적인 어셈블리 언어 코드로 변환합니다. 이 결과물이 바로 .s
확장자를 가진 어셈블리 파일이며, 이것이 as
가 처리할 입력 데이터가 됩니다. 이 단계에서 대부분의 문법 오류 검사와 코드 최적화가 이루어집니다.
3. 어셈블 (Assembly)
이 단계의 주인공이 바로 어셈블러(as)입니다. as
는 컴파일러가 생성한 .s
파일을 받아, mov
, add
와 같은 어셈블리 니모닉(mnemonic)을 실제 CPU가 이해할 수 있는 이진 코드, 즉 기계어로 번역합니다. 이 결과물은 오브젝트 파일(.o
)로 저장됩니다. 오브젝트 파일에는 프로그램의 코드와 데이터가 담겨 있지만, printf
와 같은 외부 라이브러리 함수의 주소를 알지 못하는 등 아직은 불완전한 상태이므로 바로 실행할 수는 없습니다.
4. 링킹 (Linking)
마지막 단계는 링커(ld)가 수행합니다. 링커는 하나 이상의 오브젝트 파일들과 프로그램에 필요한 라이브러리 파일들을 하나로 합치는 작업을 합니다. 이 과정에서 오브젝트 파일들이 참조하던 외부 심볼(함수나 변수)의 주소를 찾아 연결하고, 모든 조각을 모아 최종적으로 운영체제가 실행할 수 있는 완전한 실행 파일을 만들어냅니다.
GNU 툴체인 생태계 (Binutils)
as
는 단독으로 존재하는 도구가 아니라, GNU Binutils라는 강력한 바이너리 유틸리티 모음의 일부입니다. 이 생태계에는 as
와 긴밀하게 협력하는 여러 도구들이 포함되어 있습니다.
- ld: 링커, 오브젝트 파일들을 묶어 실행 파일을 생성합니다.
- objdump: 오브젝트 파일의 구조를 분석하거나 기계어를 다시 어셈블리 코드로 역어셈블(disassemble)합니다.
- nm: 오브젝트 파일에 포함된 심볼(함수, 변수 이름 등) 목록을 보여줍니다.
- strip: 실행 파일에서 디버깅 심볼 등을 제거하여 파일 크기를 줄입니다.
이처럼 as
는 바이너리를 다루는 정교하고 강력한 시스템의 한 축을 담당하고 있습니다.
과정 시각화: GCC로 단계별 파일 생성하기
추상적인 컴파일 과정을 눈으로 직접 확인하는 가장 좋은 방법은 GCC의 옵션을 사용하여 각 단계의 중간 결과물을 생성해보는 것입니다.
# 1. 전처리: main.c -> main.i
gcc -E main.c -o main.i
# 2. 컴파일: main.i -> main.s (어셈블리 코드 생성)
gcc -S main.i -o main.s
# 3. 어셈블: main.s -> main.o (오브젝트 파일 생성)
gcc -c main.s -o main.o
# 4. 링킹: main.o -> main (최종 실행 파일 생성)
gcc main.o -o main
물론 gcc main.c -o main
이라는 단 하나의 명령만으로 이 모든 과정이 자동으로 수행됩니다. GCC는 내부적으로 as
와 ld
같은 도구들을 순서대로 호출하는 '드라이버' 역할을 하는 것입니다.
이러한 GCC의 표준 컴파일 파이프라인은 중요한 설계 철학을 드러냅니다. C -> Assembly -> Object Code 흐름에서 컴파일러(gcc)의 역할은 어셈블리(.s 파일)를 생성하는 것이고, 어셈블러(as)의 역할은 그것을 소비하는 것입니다. 이는 as
가 처음부터 인간 개발자를 위한 도구라기보다는, 컴파일러가 생성한 결과물을 처리하기 위해 최적화된 도구라는 점을 시사합니다. 뒤이어 다룰 AT&T 문법의 '투박함'은 설계 결함이 아니라, 기계 생성에 최적화된 정밀하고 모호하지 않은 표현 방식이라는 관점에서 이해할 수 있습니다.
세기의 문법 대결: AT&T vs. Intel
x86 어셈블리 언어를 처음 접할 때 가장 큰 장벽 중 하나는 바로 두 가지 주요 문법, AT&T와 Intel의 존재입니다. 이 둘은 단순히 스타일의 차이를 넘어, 각기 다른 컴퓨팅 역사와 철학을 반영합니다.
- AT&T 문법: GNU 툴체인(
as
,gdb
,objdump
)의 기본 문법입니다. 그 뿌리는 유닉스와 PDP-11의 역사에 닿아 있으며, 리눅스를 포함한 유닉스 계열 시스템의 표준 언어와도 같습니다. - Intel 문법: Intel의 공식 CPU 문서에 사용되는 문법으로, NASM, FASM, MASM과 같은 어셈블러를 통해 DOS 및 Windows 환경에서 지배적인 위치를 차지하고 있습니다.
두 문법의 핵심적인 차이점은 다음 표와 같습니다. 이 표는 두 생태계를 오가는 개발자들에게 유용한 빠른 참조 가이드가 될 것입니다.
표 1: AT&T (GAS) vs. Intel (NASM) 문법 핵심 비교
기능 | AT&T (GAS) 문법 | Intel (NASM) 문법 | 설명 |
---|---|---|---|
피연산자 순서 | movl %eax, %ebx |
mov ebx, eax |
소스(Source)가 먼저, 목적지(Destination)가 나중입니다. (move from source to destination) |
레지스터 접두사 | %eax, %rbx |
eax, rbx |
모든 레지스터 이름 앞에 '%' 기호를 붙입니다. |
즉시값 접두사 | movl $5, %eax |
mov eax, 5 |
상수, 즉 즉시값(Immediate) 앞에 '$' 기호를 붙입니다. |
피연산자 크기 | movb, movw, movl, movq |
mov (크기 추론) |
명령어 니모닉에 크기 접미사(b: byte, w: word, l: long, q: quad)를 붙여 명시적으로 크기를 지정합니다. |
메모리 피연산자 | movl foo, %eax |
mov eax, [foo] |
메모리 주소의 내용을 참조할 때, Intel 문법은 대괄호 [] 를 사용합니다. |
메모리 주소 지정 | movl (%ebx), %eax |
mov eax, [ebx] |
레지스터가 가리키는 주소를 참조(간접 주소 지정)할 때 소괄호 ()를 사용합니다. |
복합 주소 지정 | movl -4(%ebp, %eax, 4), %edx |
mov edx, [ebp + eax*4 - 4] |
disp(base, index, scale) 라는 독특한 형식을 사용합니다. |
주석 | # 주석 또는 /* 주석 */ |
; 주석 |
C나 셸 스크립트와 유사한 주석 스타일을 지원합니다. |
GAS에서 Intel 문법 사용하기
다행히도 as
는 Intel 문법에 익숙한 개발자들을 위해 실용적인 해결책을 제공합니다. 바로 .intel_syntax noprefix
지시어입니다. 이 지시어를 사용하면 GAS 파일 내에서도 Intel 문법으로 코드를 작성할 수 있습니다.
# AT&T 기본 문법
movl $10, %eax # eax 레지스터에 10을 저장
# Intel 문법으로 전환
.intel_syntax noprefix
mov eax, 10 # eax 레지스터에 10을 저장
# 다시 AT&T 문법으로 복귀 (필요 시)
.att_syntax
이처럼 문법의 차이는 기술적인 것을 넘어 문화적, 역사적 배경을 담고 있습니다. 유닉스 계열 툴체인의 심장인 GNU 어셈블러가 경쟁 진영의 문법을 지원하기로 한 결정은, 소프트웨어 도구가 독단주의보다는 실용주의를 택하며 발전해왔음을 보여주는 중요한 사례입니다. 이는 개발자 커뮤니티의 현실을 인정하고 더 넓은 사용자층을 포용하려는 노력의 산물이며, GNU 툴체인의 접근성을 크게 높였습니다.
나의 첫 어셈블리 프로그램: 리눅스 x86-64에서 "Hello, World!"
이론을 배웠으니 이제 직접 어셈블리 코드를 작성하고 실행해볼 차례입니다. 가장 고전적인 "Hello, World!" 프로그램을 통해 as
의 작동 방식과 리눅스 시스템 콜의 원리를 깊이 있게 살펴보겠습니다. 여기서는 C 라이브러리의 도움 없이, 운영체제 커널과 직접 통신하는 가장 순수한 형태의 프로그램을 작성합니다.
코드 해부: 한 줄 한 줄 뜯어보기
다음은 AT&T 문법으로 작성된 최소한의 "Hello, World!" 프로그램입니다.
# 파일명: hello.s
.section.data
msg:
.ascii "Hello, World!\n"
len = . - msg
.section.text
.global _start
_start:
# write(1, msg, len) 시스템 콜 호출
mov $1, %rax # 시스템 콜 번호 1 (write)
mov $1, %rdi # 파일 디스크립터 1 (stdout)
mov $msg, %rsi # 출력할 메시지 주소
mov $len, %rdx # 메시지 길이
syscall
# exit(0) 시스템 콜 호출
mov $60, %rax # 시스템 콜 번호 60 (exit)
xor %rdi, %rdi # 종료 코드 0 (mov $0, %rdi와 동일하나 더 효율적)
syscall
.section .data
: 초기화된 데이터가 위치할 데이터 섹션을 선언합니다.msg: .ascii "..."
:msg
라는 레이블에 "Hello, World!\n" 문자열을 바이트 시퀀스로 정의합니다.len = . - msg
:.
은 현재 위치를 나타내는 특수 심볼입니다. 현재 위치 -msg
레이블의 시작 주소를 계산하여 문자열의 길이를len
이라는 심볼에 저장합니다..section .text
: 실행 코드가 위치할 텍스트 섹션을 선언합니다..global _start
:_start
라는 심볼을 외부 링커가 볼 수 있도록 전역(global)으로 만듭니다. 링커는 이_start
레이블을 프로그램의 진입점(entry point)으로 인식합니다.syscall
: x86-64 아키텍처에서 커널의 서비스를 요청하는 명령어입니다. 오래된int 0x80
인터럽트 방식을 대체하는 현대적인 방법입니다.
리눅스 시스템 콜 심층 분석
위 예제의 mov
명령어들은 임의의 숫자를 레지스터에 넣는 것이 아닙니다. 이는 x86-64 System V ABI(Application Binary Interface)라는 엄격한 '규칙'에 따른 것입니다. 이 ABI는 프로그램이 커널과 어떻게 상호작용해야 하는지를 정의하며, 어떤 레지스터에 어떤 값을 넣어야 하는지에 대한 규약을 포함합니다. 아래 표는 이 규칙을 이해하는 '로제타석' 역할을 합니다.
표 2: x86-64 리눅스 시스템 콜 주요 레지스터 규약 (ABI)
레지스터 | 목적 | write(1, msg, 13) 예시 |
exit(0) 예시 |
---|---|---|---|
rax |
시스템 콜 번호 | 1 | 60 |
rdi |
인자 1 | 1 (stdout) | 0 (성공 종료 코드) |
rsi |
인자 2 | msg (버퍼 주소) |
N/A |
rdx |
인자 3 | len (바이트 수) |
N/A |
rcx |
인자 4 | N/A | N/A |
r8 |
인자 5 | N/A | N/A |
r9 |
인자 6 | N/A | N/A |
rax |
반환 값 | 쓰여진 바이트 수 또는 에러(-errno) | 반환하지 않음 |
이 표를 보면 write
시스템 콜(번호 1)을 호출하기 위해 왜 rax
에 1을, 첫 번째 인자인 파일 디스크립터(stdout)를 위해 rdi
에 1을 넣는지 명확히 이해할 수 있습니다.
소스에서 실행까지: 두 가지 경로
작성한 hello.s
파일을 실행 파일로 만드는 방법은 두 가지가 있습니다.
경로 1: 수동 방식 (as와 ld 직접 사용)
이 방법은 툴체인의 각 단계를 직접 실행하여 내부 동작을 명확히 보여줍니다.
# 어셈블: hello.s -> hello.o
as hello.s -o hello.o
# 링크: hello.o -> hello
ld hello.o -o hello
경로 2: 드라이버 방식 (gcc 사용)
이것이 더 간단하고 일반적인 방법입니다. gcc가 as
와 ld
를 자동으로 호출해줍니다.
# 어셈블과 링크를 한 번에
gcc hello.s -o hello
두 경로 모두 ./hello
명령으로 실행하면 "Hello, World!"가 화면에 출력됩니다.
여기서 _start
와 syscall
을 사용한 방식과, C언어에서처럼 main
함수와 printf
같은 라이브러리 함수를 사용하는 방식 사이에는 근본적인 차이가 있습니다. main
함수를 사용한다는 것은 C 런타임(CRT)과 표준 라이브러리 전체를 프로그램에 링크한다는 의미입니다. 이는 편리함을 제공하지만, 프로그램의 크기를 상당히 증가시킵니다. 반면, _start
를 사용하는 것은 CRT나 라이브러리 없이 오직 커널과 직접 대화하는, 작고 독립적인 프로그램을 만드는 것을 의미합니다. 이는 제어권과 크기, 그리고 편의성과 이식성 사이의 중요한 트레이드오프를 보여줍니다.
as 명령어 마스터하기: 핵심 옵션과 지시어
gcc
가 편리한 드라이버이긴 하지만, 어셈블 과정을 세밀하게 제어하거나 디버깅하기 위해서는 as
의 커맨드 라인 옵션을 직접 사용하는 것이 필수적입니다.
표 3: GNU Assembler (as) 필수 커맨드 라인 옵션
옵션 | 설명 | 예시 사용법 |
---|---|---|
-o <file> |
출력 오브젝트 파일의 이름을 지정합니다. 지정하지 않으면 a.out이 생성됩니다. | as code.s -o code.o |
-a[cdghlns] |
다양한 형식의 어셈블리 리스팅 파일을 생성합니다. (d: 디버깅, h: 소스, l: 어셈블리, s: 심볼 테이블) | as -alhs=code.lst code.s |
-g / --gen-debug |
디버깅 정보를 생성합니다. GDB에서 소스 코드 레벨 디버깅을 가능하게 합니다. | as -g code.s -o code.o |
--defsym <sym>=<val> |
어셈블리 시작 전에 심볼 sym의 값을 val로 정의합니다. 조건부 컴파일에 유용합니다. | as --defsym DEBUG=1 code.s |
-I <dir> |
.include 지시어가 파일을 검색할 디렉터리를 추가합니다. |
as -I./includes code.s |
-W / --no-warn |
모든 경고 메시지를 출력하지 않습니다. | as -W code.s |
--fatal-warnings |
경고가 발생하면 에러로 처리하여 어셈블을 중단시킵니다. | as --fatal-warnings code.s |
--statistics |
어셈블리에 소요된 시간과 최대 메모리 사용량 같은 통계 정보를 출력합니다. | as --statistics code.s |
-march=<arch> |
특정 CPU 아키텍처를 타겟으로 지정합니다. 크로스 컴파일 시 매우 중요합니다. | arm-none-eabi-as -march=armv7-a code.s |
필수 어셈블러 지시어
지시어(Directives)는 CPU 명령어가 아니라, 어셈블러에게 특정 작업을 수행하도록 지시하는 명령어입니다. 데이터 정의, 섹션 배치, 심볼 관리 등 어셈블리 과정 전반을 제어합니다.
데이터 정의:
.byte, .short, .long, .quad
: 각각 1, 2, 4, 8바이트 크기의 정수를 정의합니다..ascii, .asciz / .string
: 문자열을 정의합니다..asciz
는 C 스타일로 문자열 끝에 널(null) 문자를 자동으로 추가합니다.
심볼 관리:
.global <symbol>
: 심볼을 다른 파일에서 참조할 수 있도록 전역으로 만듭니다..local <symbol>
: 심볼을 현재 파일 내에서만 사용하도록 지역으로 제한합니다..equ <symbol>, <value> / .set...
: 심볼에 특정 상수 값을 할당합니다.
섹션 제어:
.section .text, .section .data, .section .bss
: 코드가 위치할.text
섹션, 초기화된 데이터가 위치할.data
섹션, 초기화되지 않은 데이터가 위치할.bss
섹션을 지정합니다.
정렬 및 채우기:
.align <boundary>
: 다음 데이터나 코드를 지정된 바이트 경계에 맞추어 정렬합니다. 성능 최적화에 중요합니다..skip <size> / .space <size>
: 지정된 바이트만큼 공간을 건너뜁니다(주로 0으로 채움).
고급 기법: 재사용 가능한 강력한 매크로 작성법
어셈블리 프로그래밍에서 반복적인 코드 시퀀스를 줄이고 가독성을 높이는 가장 강력한 도구는 매크로(Macro)입니다.
as
의 매크로 기능은 단순한 텍스트 치환을 넘어, 조건부 어셈블리와 결합하여 고도로 추상화된 구조를 만들 수 있게 해줍니다.
매크로의 기초
- 정의와 종료: 매크로는
.macro
지시어로 시작하고.endm
지시어로 끝납니다. - 인자 전달: 매크로 이름 뒤에 인자 이름을 나열하여 값을 전달받을 수 있습니다. 매크로 본문 내에서는 백슬래시(
\
)를 인자 이름 앞에 붙여 참조합니다 (예:\arg1
). - 지역 레이블: 매크로가 여러 번 호출될 때 레이블 이름이 중복되는 문제를 피하기 위해,
\@
라는 특수 심볼을 사용합니다. 어셈블러는 매크로를 확장할 때마다\@
를 고유한 숫자로 대체하여 충돌을 방지합니다.
조건부 어셈블리
매크로에 논리를 부여하는 기능입니다. .if
, .ifeq
(if equal), .ifne
(if not equal), .else
, .endif
와 같은 지시어를 사용하여 특정 조건이 참일 때만 코드를 어셈블하도록 할 수 있습니다. 예를 들어, DEBUG
심볼의 값에 따라 디버깅 코드를 포함하거나 제외하는 매크로를 만들 수 있습니다.
실용 예제: 재귀적 sum 매크로
as
공식 문서에 나오는 이 예제는 인자 전달, 재귀 호출, 조건부 어셈블리를 하나의 우아한 코드로 보여줍니다.
# 0부터 5까지의 숫자를 .long 지시어로 생성하는 매크로
.macro sum from=0, to=5
.long \from
.if \to-\from # 'to'가 'from'보다 클 경우에만 재귀 호출
sum "(\from+1)", \to
.endif
.endm
.section .data
# 매크로 호출
sum 0, 5
위 코드를 어셈블하면 다음과 같이 확장됩니다.
.long 0
.long 1
.long 2
.long 3
.long 4
.long 5
이러한 기능들의 조합은 단순한 어셈블리 프로그래밍의 차원을 넘어섭니다. as
의 매크로와 조건부 어셈블리를 활용하면 if/else, while 루프와 같은 구조적 프로그래밍 구문을 직접 구현할 수 있습니다. 실제로 이를 구현한 매크로 라이브러리도 존재합니다. 이는 어셈블러가 단순한 번역기를 넘어, 개발자가 자신만의 도메인 특화 언어(DSL)를 구축할 수 있는 강력한 메타프로그래밍 시스템임을 보여줍니다.
크로스 컴파일 입문: x86에서 ARM을 향하여
크로스 컴파일(Cross-compilation)은 현재 사용 중인 시스템(예: x86-64 PC)에서 다른 아키텍처(예: ARM 프로세서)를 위한 실행 파일을 만드는 과정을 말합니다. 개발용 PC의 성능이 훨씬 뛰어난 임베디드 시스템, IoT 기기, 모바일 장치 개발에 있어 이는 선택이 아닌 필수 기술입니다.
GNU 크로스 툴체인
GNU 툴체인은 표준화된 이름 규칙을 통해 크로스 컴파일을 체계적으로 지원합니다. 툴체인의 이름은 보통 <arch>-<vendor>-<os>-<abi>
형식으로 구성됩니다 (예: aarch64-linux-gnu
). 데비안/우분투 계열 리눅스에서 apt install gcc-aarch64-linux-gnu
와 같은 명령어를 실행하면, gcc뿐만 아니라 aarch64-linux-gnu-as
, aarch64-linux-gnu-ld
등 타겟 아키텍처에 맞춰진 전체 툴체인이 설치됩니다.
실전: x86에서 AArch64용 "Hello, World!" 컴파일하기
툴체인 설치 (데비안/우분투 기준)
sudo apt update
sudo apt install gcc-aarch64-linux-gnu
AArch64 어셈블리 코드 작성 (hello_arm.s)
AArch64는 x86-64와 시스템 콜 번호 및 레지스터 사용 규칙이 다릅니다.
.data
msg: .asciz "Hello, ARM World!\n"
len = . - msg
.text
.global _start
_start:
// write(1, msg, len)
mov x0, 1 // 파일 디스크립터 (stdout) -> x0 레지스터
ldr x1, =msg // 메시지 주소 -> x1 레지스터
mov x2, len // 메시지 길이 -> x2 레지스터
mov x8, 64 // AArch64의 write 시스템 콜 번호 -> x8 레지스터
svc 0 // 시스템 콜 호출
// exit(0)
mov x0, 0 // 종료 코드 0 -> x0 레지스터
mov x8, 93 // AArch64의 exit 시스템 콜 번호 -> x8 레지스터
svc 0
크로스 어셈블 및 링크
반드시 타겟 아키텍처용 as
와 ld
를 사용해야 합니다.
aarch64-linux-gnu-as hello_arm.s -o hello_arm.o
aarch64-linux-gnu-ld hello_arm.o -o hello_arm
결과 확인
생성된 파일이 정말 ARM용인지 file
명령어로 확인할 수 있습니다.
$ file hello_arm
hello_arm: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, no section header
출력 결과가 ARM aarch64를 명시하고 있음을 볼 수 있습니다. 이 파일은 x86 PC에서는 실행되지 않으며, QEMU 같은 에뮬레이터나 실제 ARM 장치에서 실행해야 합니다.
GNU 툴체인의 잘 정립된 크로스 컴파일 지원은 전체 오픈소스 임베디드 생태계를 지탱하는 기반 기술입니다. 만약 이러한 도구가 없다면, 개발자는 매번 지극히 복잡한 과정을 거쳐 직접 크로스 컴파일러를 빌드해야 할 것입니다. aarch64-linux-gnu-as
와 같은 도구는 as
가 단지 x86 리눅스 시스템만을 위한 도구가 아니라, 현대의 거의 모든 프로세서를 위한 소프트웨어를 구축하는 '다리' 역할을 한다는 것을 보여줍니다.
결론: 어셈블러를 아는 개발자의 힘
지금까지 우리는 as
, 즉 GNU 어셈블러의 세계를 깊이 있게 탐험했습니다. 컴파일 과정에서의 역할부터 AT&T와 Intel 문법의 차이, 시스템 콜의 작동 원리, 그리고 매크로와 크로스 컴파일이라는 고급 기법에 이르기까지, as
는 단순히 오래된 도구가 아니라 현대 소프트웨어 개발의 가장 낮은 계층을 떠받치는 핵심 기둥임을 확인했습니다.
컴파일러와 하드웨어 사이의 계층을 이해하는 능력은 결코 낡은 기술이 아닙니다. 이는 성능을 한계까지 끌어올리는 최적화, 원인을 알기 힘든 버그를 추적하는 로우레벨 디버깅, 보안 취약점 분석과 리버스 엔지니어링 등 고급 개발자가 갖추어야 할 핵심 역량과 직결됩니다.
이 글을 읽는 데 그치지 말고, 직접 실천해보기를 권장합니다. 자신만의 작은 어셈블리 프로그램을 작성해보고, 평소 사용하던 C/C++ 코드를 gcc -S
옵션으로 컴파일하여 그 결과물을 분석해보십시오. 소프트웨어가 하드웨어와 어떻게 상호작용하는지 직접 눈으로 확인하는 경험은, 더 깊은 수준의 이해와 문제 해결 능력을 선사할 것입니다. 어셈블러를 이해하는 것은 개발자에게 세상을 다르게 보는 눈을 뜨게 해주는 강력한 무기입니다.
오늘날의 복잡한 IT 환경에서 시스템 보안과 규정 준수를 위해서는 단순히 '누가' 시스템에 접속했는지 아는 것만으로는 부족합니다. '언제', '어디서' 접속했는지에 대한 정보가 위변조가 불가능한 신뢰할 수 있는 로그에 기록되고 관리되어야 합니다. 많은 시스템 관리자들이 사용자 접속 기록을 확인할 때 last
명령어를 습관적으로 사용하지만, 이 명령어는 근본적인 한계를 가지고 있습니다. last
명령어는 /var/log/wtmp
파일을 기반으로 동작하는데, 이 파일은 손상되거나 악의적인 사용자에 의해 변조될 가능성이 있습니다.
이러한 한계를 극복하기 위해 등장한 것이 바로 aulast
명령어입니다. aulast
는 단순한 로그 조회 도구가 아니라, 리눅스 시스템의 강력한 보안 기능인 리눅스 감사 프레임워크(auditd
)를 기반으로 동작하는 전문적인 감사 도구입니다. 이를 통해 시스템 관리자와 보안 전문가는 신뢰할 수 있고 상세한 사용자 로그인 기록을 확보하여 보안 감사 및 침해 사고 분석에 활용할 수 있습니다. 본문에서는 aulast
와 last
의 근본적인 차이점부터 시작하여 aulast
의 고급 활용법, 그리고 이 모든 것을 가능하게 하는 auditd
시스템의 핵심까지 심도 있게 다룰 것입니다.
aulast 명령어란 무엇인가?
aulast
명령어의 정체성과 목적을 명확히 이해하는 것은 리눅스 시스템 보안 감사의 첫걸음입니다. 이 섹션에서는 aulast
가 일반적인 로그 조회 도구와 어떻게 다르며, 왜 보안 전문가에게 필수적인지 그 이유를 파헤칩니다.
aulast의 핵심 목적: 보안 감사 로그 기반의 사용자 추적
aulast
명령어의 핵심 목적은 시스템에 마지막으로 로그인한 사용자 목록을 보여주는 것입니다. 이는 last
명령어와 유사해 보이지만, 결정적인 차이는 바로 데이터의 출처에 있습니다. aulast
는 리눅스 감사 데몬(auditd
)이 생성하는 보안 감사 로그, 통상적으로 /var/log/audit/audit.log
파일에서 정보를 읽어옵니다.
이것이 의미하는 바는 매우 중요합니다. aulast
는 단순한 접속 기록 유틸리티가 아니라, 시스템의 보안 감사 프로세스 그 자체의 일부라는 것입니다. 감사 로그는 시스템에서 발생하는 보안 관련 이벤트를 매우 상세하게 기록하도록 설계되었으며, aulast
는 이 신뢰도 높은 데이터를 활용하여 관리자와 보안 담당자가 사용자의 활동을 정확하게 추적하고, 잠재적인 악의적 행위를 식별하며, 각종 보안 규정을 준수할 수 있도록 돕습니다.
last 명령어와의 결정적 차이점
aulast
와 last
의 차이점을 명확히 이해하면 언제 어떤 명령어를 사용해야 할지 판단할 수 있습니다. 두 명령어의 역할은 명확하게 구분됩니다.
- 데이터 소스(Data Source):
aulast
는 보안을 위해 설계된/var/log/audit/audit.log
를 사용하지만,last
는 일반적인 로그인 정보를 기록하는/var/log/wtmp
파일을 사용합니다. - 출력 순서(Output Order):
aulast
는 가장 오래된 기록부터 최신 기록 순서(시간순)로 이벤트를 표시하여 사건의 발생 순서를 추적하기에 직관적입니다. 반면,last
는 가장 최신 기록부터 오래된 기록 순서(역시간순)로 출력합니다. - 정보 범위(Information Scope): 감사 시스템은 모든 터미널(TTY/PTY) 할당을 기록하지는 않기 때문에,
aulast
는last
에 비해 터미널 할당 관련 기록이 적게 나타날 수 있습니다. 이는aulast
가 보안적으로 의미 있는 '로그인' 이벤트에 더 집중하기 때문입니다.
두 명령어의 차이점은 다음 표를 통해 명확하게 비교할 수 있습니다. 이 표는 사용자가 자신의 목적(단순 확인인지, 보안 감사인지)에 따라 어떤 도구를 선택해야 할지 즉각적으로 판단하는 데 도움을 줍니다.
기능/특징 (Feature) | aulast |
last |
---|---|---|
데이터 소스 (Data Source) | /var/log/audit/audit.log (보안 감사 로그) |
/var/log/wtmp (일반 로그인 로그) |
신뢰성/보안 (Reliability/Security) | 높음 (root도 임의 수정이 어려움) | 보통 (파일 손상 또는 수정에 상대적으로 취약) |
출력 순서 (Output Order) | 오래된 기록부터 최신 기록 순 (시간순) | 최신 기록부터 오래된 기록 순 (역시간순) |
정보 상세도 (Information Detail) | 감사 이벤트 ID 등 포렌식에 유용한 추가 정보 제공 가능 | 로그인/로그아웃, 터미널, IP 주소 등 기본 정보 제공 |
주요 사용 목적 (Primary Use Case) | 보안 감사, 침해 사고 분석, 규정 준수 | 일반적인 시스템 사용자 접속 이력 확인 |
의존성 (Dependencies) | auditd 서비스가 실행 중이어야 함 |
기본 시스템 로깅으로 동작 |
왜 wtmp가 아닌 감사 로그(audit.log)를 사용할까?
aulast
가 보안 감사에 더 적합한 이유는 전적으로 데이터 소스의 신뢰성 때문입니다. last
가 사용하는 wtmp
파일은 단순한 바이너리 파일로, 시스템 비정상 종료 등으로 인해 쉽게 손상될 수 있으며, 이 경우 출력에 'crash'와 같은 부정확한 정보가 표시될 수 있습니다. 또한, 루트 권한을 탈취한 공격자에 의해 변조될 가능성도 상대적으로 높습니다.
반면, aulast
가 의존하는 리눅스 감사 프레임워크(auditd
)는 처음부터 보안과 규정 준수(예: CAPP)를 목표로 설계되었습니다. 감사 로그 파일(/var/log/audit/audit.log
)은 기본적으로 root
사용자만 소유하고 제거할 수 있어 변조가 매우 어렵습니다. 더 나아가, auditd
데몬은 로그를 기록할 수 없는 상황(예: 디스크 공간 부족)이 발생했을 때, 보안 정책에 따라 시스템을 즉시 정지시켜 단 하나의 감사 이벤트도 놓치지 않도록 설정할 수 있는 강력한 실패 방지 메커니즘을 갖추고 있습니다.
결론적으로, aulast
명령어에 대한 신뢰는 명령어 자체의 특별함이 아니라, 그 기반이 되는 리눅스 감사 프레임워크 전체의 무결성과 견고함에서 비롯됩니다. 따라서 모든 종류의 보안 조사나 침해 사고 분석에서는 aulast
를 사용하는 것이 전문가적인 표준 절차입니다.
aulast 명령어 기본 사용법 및 주요 옵션
이론을 넘어 실제 aulast
명령어를 사용하는 방법을 익혀보겠습니다. 기본 구문부터 자주 사용하는 옵션까지, 이 섹션을 통해 aulast
를 능숙하게 다룰 수 있는 기초를 다질 수 있습니다.
기본 구문 및 출력 형식 이해하기
가장 기본적인 사용법은 터미널에 단순히 명령어를 입력하는 것입니다.
aulast
명령을 실행하면 다음과 같은 형식의 출력을 볼 수 있습니다. 각 열은 중요한 정보를 담고 있습니다.
- Username: 로그인한 사용자 계정 이름
- TTY: 사용자가 접속한 터미널 (예:
tty1
은 로컬 콘솔,pts/0
은 원격 SSH 세션) - Hostname/IP: 사용자가 접속한 원격 호스트의 이름 또는 IP 주소
- Login Time: 사용자가 로그인한 날짜와 시간
- Logout Time/Status: 사용자가 로그아웃한 시간 또는 현재 상태 (예: 'still logged in')
- Duration: 세션이 유지된 시간
이 출력 형식은 사용자의 접속 이력을 한눈에 파악하는 데 매우 유용합니다.
자주 사용하는 필수 옵션 상세 분석
aulast
는 다양한 옵션을 통해 원하는 정보만 필터링하여 볼 수 있는 강력한 기능을 제공합니다.
--bad
: 로그인 실패 기록만 필터링하여 보여줍니다. 이 옵션은 무차별 대입 공격(brute-force attack)이나 허가되지 않은 접근 시도를 탐지하는 데 매우 중요합니다.# 모든 실패한 로그인 시도 확인 aulast --bad
--username [user]
: 특정 사용자의 로그인 기록만 조회합니다. 특정 계정의 활동을 집중적으로 조사해야 할 때 필수적인 옵션입니다.# 'jdoe' 사용자의 로그인 기록만 확인 aulast --username jdoe
--tty [tty]
: 특정 터미널에서 발생한 활동만 필터링합니다. 로컬 콘솔 접근과 원격 SSH 접근을 구분하여 분석할 때 유용합니다.# 원격 접속 세션(pts/0)의 기록만 확인 aulast --tty pts/0
-f [file]
: 기본 감사 로그 파일(audit.log
) 대신, 백업되었거나 로테이션된 특정 로그 파일을 지정하여 분석합니다. 과거 특정 시점의 로그를 분석하거나 포렌식 조사를 수행할 때 핵심적인 기능입니다.# 백업된 감사 로그 파일 분석 aulast -f /var/log/audit/audit.log.1
aulast를 활용한 실전 시나리오 및 고급 분석 기법
aulast
의 진정한 힘은 다른 감사 도구와 결합하여 고급 분석을 수행할 때 발휘됩니다. 이 섹션에서는 실전에서 마주할 수 있는 시나리오를 바탕으로 aulast
를 활용한 포렌식 기법을 소개합니다.
ausearch와 파이프라인(|) 연동: 강력한 시너지 효과
aulast
의 잠재력은 ausearch
명령어와 함께 사용될 때 극대화됩니다. 바쁜 서버에서 aulast
만 단독으로 실행하면 분석하기 어려울 정도로 방대한 결과가 출력됩니다. ausearch
는 감사 로그를 정밀하게 검색하고 필터링하는 전용 도구로, 두 명령어를 파이프라인(|
)으로 연결하는 것이 전문가적인 분석 방식입니다.
이 워크플로우의 핵심은 '필터링 후 포맷팅'입니다. 먼저 ausearch
로 원하는 이벤트를 정확히 찾아낸 뒤, 그 결과를 aulast
로 넘겨 사람이 읽기 쉬운 로그인/로그아웃 형태로 재구성하는 것입니다. 이는 ausearch
의 --raw
옵션으로 원본 로그 데이터를 출력하고, 이를 aulast
의 --stdin
옵션으로 받아 처리함으로써 구현됩니다.
- 특정 기간의 로그인 기록 필터링:
ausearch
의 유연한 시간 옵션을 활용하여 특정 기간의 로그인 기록을 쉽게 조회할 수 있습니다.# 어제부터 현재까지의 로그인 기록 보기 ausearch --start yesterday --end now --raw | aulast --stdin
- 특정 사용자의 실패한 로그인 시도 검색: 여러 필터를 조합하여 더욱 정밀한 검색이 가능합니다.
이 접근 방식은# 'jdoe' 사용자의 실패한 로그인 시도만 검색 (-m: 메시지 타입, -sv: 성공 여부, -ua: 사용자 계정) ausearch -m USER_LOGIN -sv no -ua jdoe --raw | aulast --stdin
aulast
의 내장 필터만 사용하는 것보다 훨씬 유연하고 강력한 분석을 가능하게 합니다.
--proof 옵션을 활용한 포렌식 분석: 의심스러운 세션 깊이 파고들기
보안 분석가가 의심스러운 로그인 세션을 발견했을 때, 다음 질문은 "그 세션 동안 사용자가 정확히 무엇을 했는가?"입니다. aulast
단독으로는 이 질문에 답할 수 없지만, --proof
옵션이 그 해답의 열쇠를 제공합니다.
--proof
옵션은 각 로그인 세션에 해당하는 고유한 감사 이벤트 일련번호(serial number)를 함께 출력합니다. 이 일련번호는 감사 로그 내에서 특정 이벤트를 식별하는 고유 ID입니다. 분석가는 이 ID를 ausearch -a <ID>
명령어에 인자로 전달하여 해당 세션과 관련된 모든 원본 로그 기록을 추출할 수 있습니다. 여기에는 사용자가 실행한 명령어, 접근한 파일, 사용한 시스템 콜 등 (감사 규칙에 따라) 모든 행적이 포함됩니다.
이러한 단계적 분석 과정은 요약 정보에서 심층 분석으로 자연스럽게 이어지는 완벽한 포렌식 워크플로우를 구성하며, 감사 도구의 전문가 수준 활용법을 보여줍니다.
--proof
옵션으로 의심스러운 세션의 이벤트 ID를 확보합니다.aulast --username suspicious_user --proof
- 출력된
ausearch
쿼리를 실행하여 해당 세션의 모든 상세 활동 내역을 확인합니다.
--extract 옵션으로 원본 로그 추출 및 보관
--extract
옵션은 aulast
보고서를 생성하는 데 사용된 원본 감사 기록을 현재 디렉터리에 aulast.log
라는 별도의 파일로 저장합니다.
이 기능은 침해 사고 대응 시 매우 유용합니다. 사고와 관련된 특정 기간이나 사용자를 식별한 후, ausearch
로 관련 이벤트를 필터링하고 그 결과를 aulast --extract
로 파이핑하면, 사건과 관련된 로그만 담긴 독립적인 파일을 생성할 수 있습니다. 이 파일은 증거 자료로 보관하거나, 보고서를 작성하거나, 다른 보안 팀과 공유하는 데 완벽한 자료가 됩니다.
시스템 재부팅 및 종료 기록 확인
감사 로그에는 'reboot'이라는 가상 사용자를 통해 시스템이 재부팅될 때마다 기록이 남습니다. 다음 명령어로 신뢰할 수 있는 재부팅 이력을 확인할 수 있습니다.
aulast reboot
last reboot
명령어보다 이 방법이 더 신뢰성이 높은 이유는, 앞서 설명했듯이 변조가 어려운 감사 로그를 기반으로 하기 때문입니다. 이는 시스템의 가용성을 감사해야 할 때 중요한 정보가 됩니다.
aulast를 더 잘 이해하기 위한 리눅스 감사 시스템(auditd)
aulast
의 기능을 온전히 활용하기 위해서는 그 배경이 되는 리눅스 감사 시스템에 대한 이해가 필수적입니다. 이 섹션은 aulast
가 '어떻게' 동작하는지를 넘어 '왜' 그렇게 동작하는지에 대한 깊이 있는 이해를 제공합니다.
리눅스 감사 프레임워크의 역할과 중요성
리눅스 감사 프레임워크는 크게 세 가지 요소로 구성됩니다: (1) 커널 수준에서 시스템 콜(syscall)을 가로채는 커널 모듈, (2) 이벤트를 디스크에 기록하는 auditd
데몬, 그리고 (3) 로그를 분석하는 사용자 공간 도구들(ausearch
, aureport
, aulast
등).
이 프레임워크의 핵심 목적은 시스템에서 발생하는 모든 보안 관련 이벤트에 대한 상세하고 안전한 기록을 제공하여, 관리자가 보안 정책 위반을 탐지하고, 침해 사고 발생 시 원인을 추적하며, 규정 준수 요건을 충족할 수 있도록 지원하는 것입니다.
auditd 관련 핵심 도구: aureport, ausearch
aulast
외에도 감사 로그를 분석하는 강력한 도구들이 있습니다.
aureport
: 감사 로그를 기반으로 다양한 요약 보고서를 생성하는 도구입니다. 예를 들어,aureport -l --summary
는 로그인 성공/실패 요약을,aureport --failed
는 시스템에서 발생한 모든 실패 이벤트에 대한 통계를 보여줍니다. 이를 통해 시스템 활동에 대한 높은 수준의 개요를 빠르게 파악할 수 있습니다.ausearch
: 앞서 여러 번 언급했듯이, 감사 로그를 정밀하게 쿼리하는 가장 강력하고 핵심적인 검색 도구입니다.aulast
와 연동하는 것 외에도, 단독으로 사용하여 파일 접근, 시스템 콜 사용 등 감사 규칙에 의해 기록된 모든 유형의 이벤트를 검색할 수 있습니다.
감사 규칙(audit.rules) 설정의 기초
리눅스 감사 시스템은 관리자가 정의한 '규칙'에 따라서만 이벤트를 기록합니다. 이 규칙들은 일반적으로 /etc/audit/rules.d/audit.rules
파일에 정의됩니다. aulast
가 로그인 정보를 보여줄 수 있는 이유도 기본적으로 로그인 이벤트를 기록하는 규칙이 활성화되어 있기 때문입니다.
감사 규칙이 어떻게 동작하는지 이해하기 위해 간단한 파일 감시 규칙 예제를 살펴보겠습니다.
# /etc/ssh/sshd_config 파일에 대한 쓰기(w) 및 속성 변경(a) 감시
-w /etc/ssh/sshd_config -p wa -k sshd_config_changes
이 규칙의 각 부분은 다음과 같은 의미를 가집니다.
-w
: 감시할 파일 또는 디렉터리 경로(watch)를 지정합니다.-p wa
: 감시할 권한(permission) 유형을 지정합니다.w
는 쓰기(write),a
는 속성 변경(attribute)을 의미합니다.-k sshd_config_changes
: 이 규칙에 의해 생성된 로그를 쉽게 찾을 수 있도록 고유한 키(key)를 부여합니다. 나중에ausearch -k sshd_config_changes
명령어로 관련 로그만 쉽게 검색할 수 있습니다.
이처럼 감사 규칙을 이해하면 우리가 ausearch
와 aulast
로 조회하는 데이터가 어떻게 생성되는지 근본적으로 파악할 수 있게 됩니다.
마무리
aulast
는 리눅스 시스템에서 사용자 접속 기록을 추적하는 가장 신뢰할 수 있고 강력한 도구입니다. 그 힘은 auditd
라는 견고한 보안 프레임워크와의 깊은 통합에서 나옵니다. 간단한 접속 확인에는 last
명령어가 편리할 수 있지만, 보안 감사, 규정 준수, 포렌식 조사와 같이 신뢰성이 절대적으로 요구되는 모든 업무에서는 aulast
, 특히 ausearch
와 연동한 aulast
의 사용이 필수적입니다.
이제 aulast
의 기본을 넘어, aureport
를 사용하여 시스템 활동에 대한 일일 요약 보고서를 생성하거나, auditctl
을 이용해 중요한 설정 파일에 대한 감시 규칙을 직접 추가해 보십시오. 이처럼 능동적인 시스템 모니터링과 감사는 잠재적인 위협을 사전에 탐지하고 안전한 시스템 환경을 유지하는 데 핵심적인 역할을 할 것입니다.
- 블로그 : www.infracody.com
이 글이 유익했나요? 댓글로 소중한 의견을 남겨주시거나 커피 한 잔의 선물은 큰 힘이 됩니다.