러스트에 대한 집단 환각

Sun, Jan 15 2023 09:00:10 KST

러스트는 과장으로 환상을 심어주어 광신도를 양산하는거 같습니다.

Zero Cost Abstractions

이미 C 언어도 zero cost abstractions 입니다. 그러나 그걸 홍보를 하지 않죠. 러스트 언어만 뭔가 대단한 것처럼 저런 식으로 홍보하는 것입니다.

러스트는 Segmentation Fault 가 없다?

러스트로 만들면 Segmentation Fault 는 발생하지 않지만 panic 이 발생하여 어플이 종료됩니다. 러스트는 Segmentation Fault 가 없다는 말은 말장난일 뿐입니다. 러스트 커뮤니티는 러스트가 세그폴트가 발생 안 한다고 홍보하여 사람들로 하여금 프로그램이 죽지 않는다는 환상을 가지게 하는데 러스트는 세그폴트 대신에 패닉 발생하여 어플이 죽습니다.

러스트 패닉

공식 예제 소스코드를 컴파일해봤습니다.

https://doc.rust-lang.org/book/ch12-02-reading-a-file.html

use std::env;
use std::fs;

fn main() {
    let args: Vec<String> = env::args().collect();

    let query = &args[1];
    let file_path = &args[2];

    println!("Searching for {}", query);
    println!("In file {}", file_path);

    let contents = fs::read_to_string(file_path)
        .expect("Should have been able to read the file");

    println!("With text:\n{contents}");
}

실행하면 이런 식으로 뻑나는겁니다.

~/snippets/minigrep/target/debug % ./minigrep
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:7:18
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

인자를 a, b 로 넣어주면 이렇게 뻑납니다.

~/snippets/minigrep/target/debug % ./minigrep a b
Searching for a
In file b
thread 'main' panicked at 'Should have been able to read the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:14:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

결국엔 c 로 하든 러스트로 하든 뻑나고 안 나고는 프로그래머의 역량에 달려있는 것입니다. 다만, c 로 만들면 저러한 경우 segmentation fault 발생하면 다행인데 발생하지 않을 경우 오작동의 소지가 있고 재수없으면 그게 보안 취약점으로 연결되는 것이지요. 그러서 러스트가 안전하다고 하는데 러스트가 만능은 아닙니다.

러스트의 Result

러스트의 Resut 는.. 아래 문서에 나오는데..

https://rinthel.github.io/rust-lang-book-ko/ch09-02-recoverable-errors-with-result.html
https://doc.rust-lang.org/std/result/

읽어봤는데 새로운 것이 전혀 없습니다. 오히려 불편할 뿐입니다. 차라리 다른 언어에 있는 try .. catch .. finally 는 편하기라도 하죠. c 언어와 비교해봐도 러스트의 Result 는 새로운 것이 없고 오히려 코딩할 때 불편하죠. C 언어에서는 에러 처리를 errno 라는 전역 변수로 관리를 합니다. errno = 숫자; 이런 식으로 대입하는 걸로 전파가 된다고 할 수 있죠. 숫자 대신에.. EINTR, EACCESS 등으로 입력해도 되고요. 사실 뭐 전역변수 errno 가 너무 편리합니다. 전역변수이니 별도의 전파 코드가 필요하지도 않죠. 그리고 에러 메시지가 이미 만들어져 있어요.

const char *err_msg = strerror (errno);
printf ("failed: %s\n", err_msg);

이렇게 사용하면 되고요. 이렇게 간편한 걸 러스트는 복잡하게 만들어놓은 겁니다.

러스트 바이너리가 거대한 이유

러스트로 뭐 만들면 libstd 를 사용하게 마련인데

% pkg list rust | grep libstd
/usr/local/lib/libstd-a88f8777e99dfdf0.so
/usr/local/lib/rustlib/wasm32-unknown-unknown/analysis/libstd-321a07457edb82e2.json
/usr/local/lib/rustlib/wasm32-unknown-unknown/analysis/libstd_detect-30504d11d0f89424.json
/usr/local/lib/rustlib/wasm32-unknown-unknown/lib/libstd-321a07457edb82e2.rlib
/usr/local/lib/rustlib/wasm32-unknown-unknown/lib/libstd_detect-30504d11d0f89424.rlib
/usr/local/lib/rustlib/x86_64-unknown-freebsd/analysis/libstd-a88f8777e99dfdf0.json
/usr/local/lib/rustlib/x86_64-unknown-freebsd/analysis/libstd_detect-df6bbfb73d0b1c99.json
/usr/local/lib/rustlib/x86_64-unknown-freebsd/lib/libstd-a88f8777e99dfdf0.rlib
/usr/local/lib/rustlib/x86_64-unknown-freebsd/lib/libstd-a88f8777e99dfdf0.so
/usr/local/lib/rustlib/x86_64-unknown-freebsd/lib/libstd_detect-df6bbfb73d0b1c99.rlib

libstd-a88f8777e99dfdf0.so 이러한 형태의 이름으로 배포가 됩니다.

러스트로 만든 hello 어플을

fn main() {
    println!("Hello World!");
}
rustc -C opt-level=s -C prefer-dynamic -C target-feature=-crt-static hello.rs

동적 링킹하게끔 컴파일 한 후 strip 하면 바이너리가 6240 바이트로 많이 작아지지만 심각한 문제가 있습니다.

% ldd ./hello
./hello:
	libstd-a88f8777e99dfdf0.so => /usr/local/lib/libstd-a88f8777e99dfdf0.so (0x801066000)
	libc.so.7 => /lib/libc.so.7 (0x801219000)
	libthr.so.3 => /lib/libthr.so.3 (0x801623000)
	libgcc_s.so.1 => /lib/libgcc_s.so.1 (0x801651000)

ldd 로 확인해보면 libstd-a88f8777e99dfdf0.so 이것이 동적 링킹이 되는데 전통적인 개념의 abi 버전으로 관리하는 것이 아니기 때문에 동적 링킹의 장점이 사라집니다. Rust 버전이 바뀌면 libstd-a88f8777e99dfdf0.so 에 동적 링킹한 어플은 작동이 되지 않습니다. 또한 rust 패키지를 삭제하면.. libstd 도 같이 삭제됩니다. 결국 안정적인 작동을 위해서는 libstd 의 정적 링킹은 필수적이기 때문에 바이너리가 거대해집니다. 그래서 러스트로 만든 어플은 바이너리 크기가 거대질 수 밖에 없습니다.

러스트로 만들면 바이너리 크기가 20배 커집니다

rg라는 유틸리티는 러스트로 만들어져 있으며 grep 의 복제판입니다. 크기를 비교해 보시죠.

debian:~$ ls -l /bin/rg /bin/grep
-rwxr-xr-x 1 root root  203072 Nov 10  2020 /bin/grep
-rwxr-xr-x 1 root root 4345184 Jan 19  2021 /bin/rg

약 21배 큽니다.

~$ ldd /bin/rg
    linux-vdso.so.1 (0x00007fffee76e000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fdf8dee2000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdf8dd0d000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fdf8e339000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fdf8dbc9000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fdf8dba7000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fdf8dba1000)

러스트로 만드면 동적 링킹을 해도 바이너리가 20배 커집니다. libstd 는 반드시 정적 링킹되어야 합니다. libstd 를 동적 링킹하면 러스트 버전 바뀔 때마다 시스템에 설치된 libstd 도 바뀌는데 libstd 는 abi 버전을 준수하지 않기 때문에 libstd 를 동적 링킹하면 러스트를 업그레이드했을 때 이전 버전의 러스트로 만든 어플이 작동하지 않겠죠. 그래서 libstd 는 반드시 정적 링킹되어야 사용상의 문제가 발생하지 않으므로 러스트로 만들면 바이너리 크기가 20배가 커질 수 밖에 없답니다. 저러한 문제점 때문에 러스트는 널리 사용되기 어렵습니다.

러스트 커뮤니티가 안내하는 min-sized-rust 의 불편한 진실

c 언어로

#include <stdio.h>

int main ()
{
  puts ("hello world");
  return 0;
};

이것은 다음과 같이 컴파일했을 때

cc -O2 -o hello hello.c

사이즈가 14680 Dec 9 11:23 hello 이렇게 됩니다. strip hello 하면 사이즈가 4960 Dec 9 11:23 hello 이렇게 됩니다.

ldd 하면

~ % ldd ./hello
./hello:
    libc.so.7 => /lib/libc.so.7 (0x800245000)

링크는 libc.so.7 달랑 하나밖에 없죠. 이렇게 나와요. 저거는 제가 무슨 꼼수를 부린게 아니라 -O2 옵션 주고.. 나중에 strip 으로 바이너리 크기를 줄입니다. 일반적으로 저렇게 합니다. 그래서 c 언어로 hello 를 만들면 바이너리 사이즈가 4,960바이트, 대충 5kb 라고 칩시다. 그런데 러스트로 만들면 바이너리가 거대해져요. 그 문제를 지적한 사람들이 엄청 많아서

https://github.com/johnthagen/min-sized-rust

이런 것도 있잖아요.

1. 일단 no-std 를 살펴보자고요.

no-std 로 할거 같으면 러스트를 쓸 일이 없죠.

저자도 이렇게 말합니다.

(구글번역) 이 접근 방식에는 많은 단점이 있음을 이해하는 것이 중요합니다. 그 중 하나는 안전하지 않은 코드를 많이 작성하고 libstd에 의존하는 대부분의 Rust 크레이트에 대한 액세스 권한을 잃어야 할 가능성이 높습니다. 그럼에도 불구하고 바이너리 크기를 줄이기 위한 하나의 옵션(극단적이기는 하지만)입니다.

아무튼 바이너리 사이즈는 5200 Dec 9 11:30 min-sized-no_std 이렇게 나옵니다. c 에 근접하는 수준이죠. 하지만.. 러스트의 장점이 없어지므로 no-std 로 컴파일하는 방식은 제외해야겠죠.

2. Optimize libstd with build-std

이것도 정상적은 방법은 아니에요. 러스트 바이너리가 엄청 크다고 제가 항상 얘기하죠? 자 보십쇼.

New packages to be INSTALLED:
    rust-nightly: 1.63.0.20220622 [FreeBSD]

Number of packages to be installed: 1

The process will require 1 GiB more space.

1기가라.. 놀랍습니다.

설치되는 과정을 보니…

Installed packages to be REMOVED:
    rust: 1.63.0

New packages to be INSTALLED:
    rust-nightly: 1.63.0.20220622 [FreeBSD]

Number of packages to be removed: 1
Number of packages to be installed: 1

The process will require 866 MiB more space.

기존의 rust 가 삭제되고.. rust-nightly 가 설치되네요. 이거는.. 제품을 생산할 때에는 저런 식으로 하면 안 되는거죠. 테스트 용도로만 써야 하는거고요. 배포할 소프트웨어 제작에 사용해서는 안 되는 방법이에요. 해보니까..

~/min-sized-rust/build_std % rustc -vV
rustc 1.63.0-nightly
binary: rustc
commit-hash: unknown
commit-date: unknown
host: x86_64-unknown-freebsd
release: 1.63.0-nightly
LLVM version: 14.0.5
hodong@nimfsoft:~/min-sized-rust/build_std % cargo +nightly build -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort --target x86_64-unknown-freebsd --release
error: no such subcommand: `+nightly`

에러나서 안되요. 그래서 포기하고..

3. no_main 이것도 정상적인 방법은 아니에요.

저자도 그걸 알기에 이렇게 설명을 합니다.

(구글번역) C 진입점을 사용하고(#![no_main] 속성을 추가하여) stdio를 수동으로 관리하고 사용자 또는 종속성이 포함하는 코드 청크를 주의 깊게 분석하면 비대한 core::fmt를 피하면서 때때로 libstd를 사용할 수 있습니다.

평소보다 더 안전하지 않은{}코드가 해킹되고 이식할 수 없을 것으로 예상합니다. no_std처럼 느껴지지만 libstd가 있습니다.

해보니까.. 그래도 크네요. (아까.. rust-nightly 설치했었는데.. rust 로 원복했어요)

255976 Dec 9 11:57 no_main

4. min-sized-rust 규범적인 방법, 정상적인 방법

이렇게 해야 당연한거죠. 1,2,3 은.. 규범적인 방법이 아니에요.

277624 Dec 9 10:35 min-sized-rust

이렇게 나오네요.

즉, 러스트로 만들면.. 바이너리가 20배~40배 커진다는 얘기에요. 이 문제를 해외, 국내에서 지적하고 있는데 러스트 커뮤니티는

https://github.com/johnthagen/min-sized-rust

이 링크 주고.. 러스트도 바이너리를 작게할 수 있다는 궤변을 늘어놓는거죠. 팩트 진단 결과 러스트로 개발하면 바이너리가 거대해집니다.

러스트 커뮤니티는 집단 환각에서 벗어나 혹세무민 그만하십시오.

러스트로 만들면 안전하다 ?

러스트는 memory-safe 하다고 알려져있잖아요. 그래서 러스트로 만들면 안전하다는 집단 환각 증세가 있는거 같아요. 자자~~~ 자료를 보시죠~~~ librsvg 라는 c 언어로 만들어진 라이브러리가 있는데..

https://gitlab.gnome.org/GNOME/librsvg

2016년 10월 경부터 러스트로 포팅하기 시작한거 같아요.

https://gitlab.gnome.org/GNOME/librsvg/-/commit/f27a8c908ac23f5b7560d2279acec44e41b91a25

2022년 12월 22일 https://github.com/GNOME/librsvg 에 나오는 언어 통계를 보면

Rust 84.9%
C 7.8%
Makefile 2.7%
Shell 2.0%
M4 1.8%
Python 0.8%

지금은.. 대부분이 러스트로 작성되어 있다는 것을 알 수 있습니다. 그래서.. librsvg 가 안전해졌는가? 자자 보시죠..

memory-safe 하다는 러스트를 사용하면 정말로 안전한 어플이 될 줄 알았겠지만.. panic 또는 faults 로 이슈를 검색해보면

https://gitlab.gnome.org/GNOME/librsvg/-/issues/?search=panic&sort=created_date&state=opened&first_page_size=20
https://gitlab.gnome.org/GNOME/librsvg/-/issues/?search=faults&sort=created_date&state=opened&first_page_size=20

현실은… 세그멘테이션 폴트에.. panic 에… 지금도 계속 진행 중입니다. 세그멘테이션 폴트 또는 panic 이 발생하면 어플이 중단/종료됩니다. 과연 그걸 안전하다고 할 수 있겠습니까? 게다가 바이너리 크기는.. c 로 개발할 때보다.. 20배 정도 커져서.. 임베디드 또는 소형 웹서버에서 librsvg 를 사용하는 사람들의 원성이 자자하죠.

이상과 현실은 다릅니다. 인터넷 검색해보면 러스트에 대한 찬양 일색인데… 실무에 러스트 적용했다가 나중에 후회할 수 있습니다. 여러 러스트 프로젝트 이슈 검색도 좀 해보고 어떠한 문제가 있는지 잘 살펴보세요~~~~

러스트의 경쟁자는 c++ ?

러스트는 memory-safe 언어로 알려져 있는데.. memory-safe 언어가 많습니다. 자바, 루비, 파이선, 자바스트립트, Zig, Crystal 등도 memory-safe 언어입니다. 그런데.. 이들 언어를 사용하는 사람들은 memory-safe 를 강조하지 않습니다. 하지만.. 러스트는 뭔가 특별한 것처럼 홍보를 하여 사람들을 홀린게 아닐까 싶습니다. 그래서 사람들은 막연히 러스트로 만들면 안전할 것이다는 착각을 하는거죠. 하지만… 자바, 루비, 파이선도 memory-safe 언어인데… 러스트 측이랑 관점이 좀 다른데… 러스트는 마땅한 존재 이유가 없어서 memory-safe 를 강조하는거 같은데.. 자바, 루비, 파이선 사용자들은… 그들이 사용하는 언어가.. 러스트처럼 memory-safe 언어인지도 모르고 사용합니다. 그러니.. 사람들은.. 자바, 루비, 파이선을 사용하여 소프트웨어를 만들면 안전하다는 착각을 하지를 않습니다. 러스트의 사용처는 어디가 될까? 러스트의 유용성은 과연 있는가? 거의 없습니다. 러스트는 c/c++의 대체제가 될 수 없으며 경쟁자도 될 수가 없습니다. abi 문제, 바이너리 사이즈 거대해지는 문제, 반쪽짜리 memory-safe, 배우기 어려운 점, 코딩할 때 생산성 떨어지는 문제 등이 그 이유입니다.

그러면 러스트의 적절한 활용처는 어디일까? 그것은 바로 성능과 보안을 동시에 요구하는 계통일 것입니다. 예를 들어 고객의 정보를 다루는 서버라던가.. 클라우드라던가.. 이런 쪽에서나 러스트를 옵션으로 고려해볼 수 있는거죠. 왜냐하면요.. c/c++ 로 만들었는데.. 허상 참조로 인하여 메모리 영역 내의 내용이 노출될 수 있는 보안 취약점이 있는데.. 이게 실제 사고로 이어지면.. 소규모 회사는 회사가 망할거고 대규모 회사는 손해액이 엄청날 것입니다. 그런 곳은 자바 쓰면 됩니다. 그런데… 자바보다 빠르면서 자바 만큼의 안전성을 기대할 수 있는 러스트가 있습니다. 다국적 기업 입장에서는… 서버 비용도 엄청나게 들텐데.. 자바보다 빨라서 서버 비용 감소하고, 자바 만큼의 안정성이 있으니 러스트 개발자를 구하기 어렵더라도.. 개발자 월급이나 포팅/개발 비용이 얼마가 들든 시도해볼 가치가 있는 것이죠.

c/c++ 은 안전하지 않아서 쓰면 안 되고 러스트를 써야한다…? 그러면 어셈블리는요? 기계어는요? CPU는요? 마찬가지로 안전장치가 없어요. 러스트로 인하여 사람들의 이성은 무뎌지고 판단력이 흐려지고 병들어 갑니다. 과연 이런 사람들이 프로그래밍을 할 수 있단 말입니까? 이렇게 판단력에 문제가 있는 사람들이 러스트로 프로그래밍하면 그 어플은 문제가 없답니까? 러스트로 만들어도 패닉 떨어져서 어플 죽습니다. 러스트로 만들어도 메모리릭 생깁니다. 메모리릭 방지되지 않고 패닉도 방지가 안 되는데 메모리 안전성 백날 외쳐봤자 그것은 말장난일 뿐입니다. 사람들이 자바에 대한 환상 깨지기까지가 한 10년 걸렸으나.. 러스트는 사용처가 거의 없다는 걸 볼 때 러스트 광신도들 그거 깨닫는데 한 10년~20년 걸릴 겁니다.

러스트는 사용처가 적습니다.

나는 러스트가 좋아요~~ 이러면서 러스트의 장미빛 미래를 기대하면서 인생을 러스트 공부에 허비하지 마십시오. 러스트가 많이 쓰인다고 좋아하지 마십시오. 그것은 침소봉대일 뿐입니다. 러스트가 아무리 많이 쓰여봤자 소프트웨어 99%는 c/c++, 파이선, 자바, 자바스크립트 등의 메이져 언어로 쓰여져 있고 러스트 지분은 많이 쳐줘야 1%입니다. 0.x% 에서 1%로 올랐다고 러스트 쓰이는데가 많다는 집단 환각에서 벗어나야 여러분들 인생이 허무하게 낭비되지 않을 것입니다. 러스트 공부할 시간에 c/c++, 자료구조를 더 공부하십시오. 교과서에서 다루는 원론적인 것들이 언어를 공부하는 것보다 더욱 보편적인 지식인 것입이다. 그래서 학교에서 가르치는 것입니다.

그렇다면 러스트의 장점은?

참고로 c 로 만들면 잘못된 메모리 참조시 세그폴트가 발생하지 않으면서 오작동해서 민감한 정보가 들어있는 메모리 영역을 참조하는 경우도 있습니다. 그게 보안 취약점으로 연결될 수 있는 것이죠.

그러나 러스트로 만들면 다음과 같은 장점이 있습니다.

  1. 엥간한 에러들을 컴파일할 때 잡아주는 것.
  2. 잘못된 메모리 참조시 패닉 발생시켜서 어플 중단시키는 것.

그래서 러스트 장점은 보안성을 향상시킬 수 있다는 것입니다.