# 내 삶에 의미(부족)를 주기 위해 aarch64 어셈블리로 웹 서버 만들기

> Clean Markdown view of GeekNews topic #29335. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=29335](https://news.hada.io/topic?id=29335)
- GeekNews Markdown: [https://news.hada.io/topic/29335.md](https://news.hada.io/topic/29335.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-05-10T06:01:47+09:00
- Updated: 2026-05-10T06:01:47+09:00
- Original source: [imtomt.github.io](https://imtomt.github.io/ymawky/)
- Points: 1
- Comments: 1

## Topic Body

- [ymawky](https://github.com/imtomt/ymawky)는 **aarch64 어셈블리**만으로 작성된 macOS용 소형 정적 HTTP 서버이며, libc 래퍼 없이 Darwin 원시 시스템 호출만 사용함
- `GET`, `HEAD`, `PUT`, `OPTIONS`, `DELETE`, 바이트 범위 요청, 디렉터리 목록, 사용자 정의 오류 페이지를 지원하지만, nginx 대체가 아니라 웹 서버 동작을 이해하기 위해 편의 계층을 제거한 구현임
- 요청 파싱, 퍼센트 디코딩, 헤더 검사, 범위 값 변환, 오류 처리, 파일 닫기, 응답 생성까지 모두 직접 작성해야 하며, Python의 간단한 문자열 분리나 `int(string)`에 해당하는 작업도 어셈블리에서는 수십~수백 줄의 검증 코드가 됨
- 서버는 새 연결마다 `fork()`를 호출하는 **fork-on-request** 구조라 구현은 쉽지만 동시 연결 처리량이 낮고 slowloris에 취약할 수 있어, 헤더 타임아웃과 `Content-Length` 기반 본문 타임아웃을 적용함
- `PUT`은 `.ymawky_tmp_&lt;pid&gt;` 임시 파일에 먼저 쓰고 성공 시 교체하며, 경로 순회 방지, `O_NOFOLLOW_ANY`, `fstat64()`, 디렉터리 목록의 URL 인코딩·HTML 이스케이프 등 파일시스템 안전성을 직접 처리함

---

### ymawky 개요와 제약
- [ymawky](https://github.com/imtomt/ymawky)는 **aarch64 어셈블리**만으로 작성된 macOS용 소형 정적 HTTP 서버임
- libc 래퍼 없이 **Darwin 원시 시스템 호출**만 사용하며, 외부 라이브러리나 기존 파서는 쓰지 않음
- 지원 기능은 `GET`, `HEAD`, `PUT`, `OPTIONS`, `DELETE`, 바이트 범위 요청, 디렉터리 목록, 사용자 정의 오류 페이지임
- 프로젝트 제약은 다음과 같음
  - aarch64 assembly only
  - macOS/Darwin 대상
  - raw syscalls only, libc wrappers 없음
  - static files only
  - preexisting parsers 없음
  - external libraries 없음
- nginx를 대체하려는 목적은 아니며, 웹 서버가 실제로 어떻게 동작하는지 이해하기 위해 편의 계층을 제거한 구현임

### 어셈블리로 웹 서버를 만들 때 필요한 작업
- 어셈블리는 기계어와 고수준 언어 사이의 계층이며, `mov`, `add`, `ldr`, `str`, `cmp` 같은 명령은 실행 바이너리의 바이트와 직접 대응함
- `svc #0x80`은 실행 바이너리의 `D4 00 10 01` 바이트에 해당하는 사람이 읽을 수 있는 형태임
- 문자열 타입이 없어서 문자열은 메모리에 연속된 바이트 영역으로 존재하며, C의 `struct` 같은 언어 기능도 없어 필드 오프셋과 전체 크기를 직접 알아야 함
- HTTP 라이브러리, 자동 정리, 예외, 객체가 없기 때문에 요청 파싱, 오류 처리, 파일 닫기, 응답 생성 같은 작업을 모두 직접 작성해야 함
- 잘못 동작해도 CPU는 경고 없이 그대로 실행하므로, 문제는 작성한 명령과 메모리 접근에 있음

### 원시 시스템 호출과 서버 흐름
- ## Darwin 시스템 호출
  - ymawky는 libc 래퍼 대신 커널을 직접 호출함
  - Darwin aarch64에서는 시스템 호출 번호를 `x16` 레지스터에 넣고, Linux aarch64에서는 `x8`에 넣음
  - `open()` 시스템 호출 번호는 `5`이며, 파일명과 모드 같은 인자를 레지스터에 직접 배치한 뒤 `svc #0x80`으로 커널을 호출함
  - `open()` 실패 시 carry flag가 설정되고, `b.cs open_failed`처럼 carry flag를 검사해 실패 처리 코드로 분기함
- ## 기본 서버 동작
  - 웹 서버의 기본 흐름은 요청을 받고, 처리하고, 상태 코드와 필요한 파일을 반환하는 구조임
  - 소켓 설정은 `socket(AF_INET, SOCK_STREAM, 0)`, `setsockopt(... SO_REUSEADDR ...)`, `bind(sockfd, &addr, 16)`, `listen(sockfd, 5)`, `accept(sockfd, NULL, NULL)` 같은 단계로 구성됨
  - ymawky는 새 연결마다 `fork()`를 호출하는 **fork-on-request** 서버임
  - 이 방식은 요청 처리 간 메모리를 공유하지 않아 이해와 구현이 쉽지만, 프로세스별 메모리 공간 때문에 부하가 커지고 nginx의 이벤트 기반 비동기 논블로킹 모델보다 동시 연결 처리량이 낮음
  - 동시 연결이 늘어나면 커널이 프로세스 내부 실행보다 프로세스 전환에 더 많은 시간을 쓰게 됨
- ## 요청 처리에서 필요한 작업
  - 요청 메서드가 `GET`, `HEAD`, `OPTIONS`, `PUT`, `DELETE` 중 무엇인지 판별함
  - 요청 경로를 추출하고 `%20` 같은 퍼센트 인코딩을 디코딩함
  - 경로 안전성 검사를 수행하고, 클라이언트가 보낸 헤더 필드를 파싱함
  - 요청 파일 정보를 가져와 디렉터리인지 일반 파일인지 구분함
  - `PUT` 요청 본문은 임시 파일에 쓰고, 응답 헤더와 본문을 생성함
  - 열린 파일을 닫고 서버가 충돌하지 않도록 오류를 처리함

### HTTP 파싱 직접 구현하기
- ## 요청 라인과 헤더 종료
  - HTTP 요청은 서버가 해석해야 하는 문자열이며, 예시는 다음과 같음
    ```http
    GET /index.html HTTP/1.0\r\n
    Range: bytes=1-5\r\n\r\n
    ```
  - 첫 줄은 `GET` 요청, 대상 파일 `index.html`, HTTP 버전 `HTTP/1.0`을 담음
  - `\r\n`은 줄의 끝이고, `\r\n\r\n`은 헤더의 끝임
  - `\r\n\r\n`을 받지 못하면 `400 Bad Request`로 중단해야 함
- ## 경로 추출
  - ymawky는 지원 메서드와 첫 바이트들을 비교해 요청 유형을 판별한 뒤 경로를 추출함
  - 헤더를 한 바이트씩 스캔해 `/` 또는 `*`를 찾지만, `HTTP/1.0` 안의 `/`를 경로로 오해하지 않도록 `/` 직전 바이트가 공백인지 확인함
  - 예를 들어 `GET HTTP/1.0\r\n\r\n`에는 `HTTP/1.0` 안에 `/`가 있으므로, 직전 바이트가 공백이 아니면 `400 Bad Request`를 반환함
  - 대부분의 시스템에서 `PATH_MAX`가 4096바이트이므로, ymawky는 4096바이트 파일명 버퍼와 널 종료 문자 1바이트를 위한 `filename_buffer: .skip 4097`을 둠
  - 요청 경로가 버퍼보다 길면 임의 메모리를 덮어쓰는 대신 `414 URI Too Long`을 반환해야 함
  - Python의 `text.split("GET /")[1].split(" ")[0]`에 가까운 작업이 어셈블리에서는 HTTP 적법성 검사까지 포함해 약 200줄이 됨
- ## 퍼센트 디코딩과 헤더 필드 검사
  - 경로에서 `%`를 만나면 다음 두 바이트가 `0-9`, `a-f`, `A-F`에 해당하는 유효한 16진수인지 확인하고, 해당 바이트 값으로 변환함
  - `GET`은 `Range:` 헤더를 가질 수 있고, `PUT`은 `Content-Length:`가 필요함
  - 이 헤더들은 요청 URL처럼 고정된 위치에 있지 않으므로 헤더 전체를 문자 단위로 순회해야 함
  - `\r` 다음에 `\n`이 없거나, 앞선 `\r` 없이 `\n`이 나오면 잘못된 헤더로 보고 `400 Bad Request`를 반환함
  - 새 헤더 줄이 공백으로 시작하면 헤더 필드는 공백으로 시작할 수 없으므로 `400 Bad Request`를 반환함
- ## 문자열 비교와 숫자 변환
  - `Range:`나 `Content-Length:`를 찾기 위해 두 문자열 포인터 `x0`, `x1`과 최대 길이 `x2`를 받아 문자 단위로 비교하는 `streqn` 함수를 작성함
  - `Range:` 헤더는 다음처럼 시작과 끝 중 하나가 생략될 수 있지만, 둘 중 하나는 반드시 있어야 함
    ```http
    Range: bytes=10-
    Range: bytes=-10
    Range: bytes=5-10
    ```
  - 범위 값은 문자열이므로 ASCII 숫자를 정수로 바꾸는 `atoi` 스타일 함수가 필요함
  - 64비트 레지스터 오버플로를 피하기 위해 숫자가 19자리 이상이면 오류로 처리함
  - Python의 `int(string)`에 해당하는 작업도 어셈블리에서는 숫자 검사, 곱셈, 덧셈, carry flag 기반 성공·실패 신호를 직접 구현해야 함

### PUT 처리와 임시 파일 전략
- `PUT`은 같은 요청을 여러 번 보내도 최종 서버 상태가 같아지는 **멱등(idempotent)** 메서드임
- `PUT /file.txt`는 `file.txt`를 만들거나 기존 파일을 완전히 덮어쓰며, `1234`를 두 번 보내도 파일 내용은 `12341234`가 아니라 `1234`임
- 전역으로 열린 `PUT`은 위험할 수 있으며, 처리 중 고려할 문제는 다음과 같음
  - 요청 처리 중 프로세스가 충돌하는 경우
  - 클라이언트가 `Content-Length`를 2KB라고 말하고 100바이트만 보내는 경우
  - 클라이언트가 `Content-Length`를 50GB처럼 매우 크게 보내는 경우
- `config.S`의 `MAX_BODY_SIZE`는 기본 1GB이며, `Content-Length`가 이를 넘으면 `413 Content Too Large`를 반환함
- 기존 파일을 바로 열어 쓰면 실패 시 반쯤 쓰인 파일이 남을 수 있으므로, ymawky는 먼저 `.ymawky_tmp_&lt;pid&gt;` 형식의 임시 파일에 씀
- `getpid()` 시스템 호출 번호 `20`으로 pid를 얻고, 사용자 정의 `itoa()`로 문자열로 변환하되 버퍼 오버플로를 검사함
- 클라이언트 본문을 임시 파일에 모두 쓰고 성공하면 임시 파일을 제자리 이름으로 바꿔 요청 파일이 서버에 생김
- 클라이언트가 예기치 않게 연결을 끊거나, 시간 초과가 나거나, 잘못된 본문을 보내면 임시 파일을 `unlink()` 시스템 호출 `10` 또는 `unlinkat()` 시스템 호출 `472`로 삭제함
- 기존 파일은 완전한 요청이 성공적으로 전송된 뒤에만 덮어씀

### 디렉터리 목록과 이스케이프 처리
- `GET /somedir/` 요청을 받으면 `config.S`의 `ALLOW_DIR_LISTING`이 켜져 있는지 확인함
- 디렉터리 목록이 비활성화되어 있으면 `403 Forbidden`을 반환함
- 활성화되어 있으면 `getdirentries64()` 시스템 호출 `344`로 요청 디렉터리의 파일 정보 버퍼를 채움
- 버퍼에는 각 파일 이름과 파일명 길이가 포함되며, ymawky는 이를 사용해 클릭 가능한 HTML을 생성함
- 각 파일에 대해 클라이언트로 보내는 기본 형태는 다음과 같음
  ```html
  &lt;a href="filename"&gt;filename&lt;/a&gt;
  ```
- `href="..."` 안의 파일명은 URL 경로 세그먼트로 **퍼센트 인코딩**해야 하고, 화면에 보이는 본문 텍스트는 **HTML 이스케이프**해야 함
- 파일명이 `&.-~><foo`이면 href는 `%26.-~%3E%3Cfoo`, 표시 텍스트는 `&.-~><foo`가 되어 최종 출력은 다음과 같음
  ```html
  &lt;a href="%26.-~%3E%3Cfoo"&gt;&.-~><foo&lt;/a&gt;
  ```
- `&lt;script&gt;something evil&lt;/script&gt;`처럼 본문 영역에서 XSS가 가능한 이름이나, `">&lt;script&gt;something dastardly&lt;/script&gt;`처럼 `href="..."` 영역에서 XSS가 가능한 이름도 실행되지 않도록 인코딩됨

### 네트워크 보안과 타임아웃
- **slowloris**는 많은 연결을 열어두고 요청을 끝내지 않아 서버 리소스를 묶어두는 서비스 거부 공격임
- ymawky는 fork-on-request 구조라 slowloris에 취약할 수 있음
- 전체 헤더가 `config.S`의 `HEADER_REQ_TIMEOUT_SECS` 안에 수신되지 않으면 `408 Request Timeout`을 보내고 연결을 닫음
- 요청 본문 수신 중 클라이언트가 너무 오래 데이터를 보내지 않으면 `config.S`의 `RECV_TIMEOUT`에 따라 같은 방식으로 처리함
- 단순한 읽기별 타임아웃만으로는 충분하지 않음
  - 악의적 클라이언트가 `Content-Length: 1073741823`을 보내고 9초마다 1바이트씩 보내면, 콘텐츠 길이는 최대치보다 1바이트 작아 허용되고 10초 단위 타임아웃에서는 300년 넘게 기다릴 수 있음
- 이를 줄이기 위해 ymawky는 `Content-Length`와 최소 초당 바이트 수를 기반으로 타임아웃을 계산함
  ```text
  timeout = grace_period + content_length / min_bps
  ```
- `grace_period`는 모든 본문에 주는 최소 시간이고, `min_bps`는 서버가 허용하는 가장 느린 전송 속도임
- 기본 `min_bps`는 16KB/s로 넉넉하지만 무한하지 않음
- 이 방식이 서비스 거부 공격을 완전히 막지는 않지만, 특정 공격이 리소스를 묶어두는 시간을 제한함

### 파일시스템 안전성
- ## 파일 정보 확인 순서
  - `GET`과 `HEAD`에서는 요청 경로를 열고 나서 파일 디스크립터에 대해 `fstat64()` 시스템 호출 `339`를 실행해 파일 종류와 크기 같은 정보를 얻음
  - 경로에 대해 먼저 `stat64()` 시스템 호출 `338`을 실행하고 그다음 파일을 열면, 검사 시점과 사용 시점 사이에 파일이 바뀌는 **TOCTOU race condition**이 생길 수 있음
- ## docroot와 경로 순회 방지
  - 모든 요청 경로 앞에는 docroot가 붙음
  - 기본 docroot는 `config.S`의 `DEFAULT_DIR`인 `www/`임
  - `/etc/shadow` 요청은 `www/etc/shadow`가 되어, `www/etc/shadow`가 실제로 존재하지 않는 한 404가 됨
  - 하지만 `/../../../../etc/shadow`는 `www/../../../../etc/shadow`가 되어 docroot 밖으로 해석될 수 있으므로 추가 방어가 필요함
  - ymawky는 단순히 문자열 `..`가 들어간 모든 경로를 거부하지 않고, 경로 세그먼트가 정확히 `..`인 경우를 거부함
  - `%2E%2E`는 디코딩 후 `..`가 되므로, 이 검사는 퍼센트 디코딩 이후에 수행해야 함
- ## 심볼릭 링크 처리
  - POSIX의 `O_NOFOLLOW` 플래그는 최종 경로 구성요소가 심볼릭 링크이면 `open()`이 실패하게 함
  - Darwin의 `O_NOFOLLOW_ANY`는 경로의 어떤 구성요소라도 심볼릭 링크이면 실패하게 함
  - docroot 안에 특정 심볼릭 링크를 심을 수 있다면 이미 다른 문제가 생긴 상태일 가능성이 크지만, 이 플래그로 추가 방어가 가능함

### Apple 전용 동작
- ## 타임아웃 처리와 `sigaction()`
  - 요청 타임아웃을 구현하려면 `setitimer()` 시스템 호출 `83`으로 일정 시간이 지난 뒤 `SIGALRM`을 보내야 함
  - 기본적으로 `SIGALRM`은 child를 죽이지만, ymawky는 먼저 `408 Request Timeout`을 보내야 함
  - 이를 위해 `sigaction()` 시스템 호출 `46`을 사용함
  - Darwin의 원시 `sigaction` 구조체는 `sa_tramp` 필드를 노출함
  - 일반적으로 libc가 `sa_tramp`를 설정해 스택과 레지스터를 저장하고 `sigreturn`을 준비한 뒤 핸들러로 분기함
  - ymawky의 타임아웃 핸들러는 `408 Request Timeout`을 보내고 필요한 항목을 닫은 뒤 child를 종료하므로 반환할 필요가 없음
  - 그래서 trampoline 슬롯을 타임아웃 응답을 직접 수행하는 코드로 가리키게 하고, `sa_handler`와 `sigreturn`을 우회함
- ## `proc_info()`와 child process 수 제한
  - Apple에는 실행 중인 프로세스와 그 자식 정보를 얻을 수 있는 잘 문서화되지 않은 `proc_info()` 시스템 호출 `336`이 있음
  - 이 호출은 보통 `ps`, `lsof`, `top` 같은 도구에서 쓰임
  - ymawky는 활성 child process 수를 세는 데 `proc_info()`를 사용함
  - 최대 연결 수가 설정 가능하므로 살아 있는 child 수를 알아야 함
  - `proc_info()`는 child process 정보를 버퍼에 쓰고, 각 요소 크기가 알려져 있으므로 기록된 바이트 수로 child 수를 계산할 수 있음
  - child 수가 `MAX_PROCS`를 넘으면 새 연결은 `503 Service Unavailable`로 거부됨

### 결론과 프로젝트 정보
- 정적 웹 서버에서 어려운 부분은 소켓을 열고 listen하는 작업보다 요청 파싱과 모든 경계 조건 처리였음
- 요청, 경로, 응답은 모두 바이트이며, 범위 요청은 정확해야 하고 파일명은 위치에 따라 다르게 이스케이프해야 함
- 어셈블리는 요청 파싱, 메모리 관리, 오류 처리, 문자열 변환, 타임아웃, 파일 안전성 같은 모든 작업을 직접 작성하게 만듦
- [ymawky](https://github.com/imtomt/ymawky)는 [imtomt](https://github.com/imtomt)가 유지 관리함

## Comments



### Comment 57133

- Author: neo
- Created: 2026-05-10T06:01:47+09:00
- Points: 1

###### [Lobste.rs 의견들](https://lobste.rs/s/wfqsc4/building_web_server_aarch64_assembly) 
- 대단하네. 예전에 스마트 기기를 만드는 작은 회사와 연동하는 일을 했는데, 그 회사의 유일한 엔지니어가 **어셈블리 언어**밖에 몰랐음  
  하드웨어 제어 코드부터 서버 운영체제, 우리가 쓰던 **JSON 웹 API**까지 전부 어셈블리로 직접 작성돼 있었음  
  한 번은 웹 API가 엉뚱한 기기의 데이터를 반환하는 버그를 만났는데, 알고 보니 운영체제 스케줄링 시스템에 오프바이원 오류가 있어서 “데이터베이스”가 웹 서비스에 잘못된 행을 돌려주고 있었음
  - 혹시 그 사람 이름이 **Mel**은 아니었나?

- “자살” 같은 표현을 다룰 때는 제발 **콘텐츠 경고**를 붙였으면 함. 더 낫게는 아예 언급하지 않는 편이 좋고
  - 뭐지? 글 일부는 대충 읽긴 했지만 처음 읽을 때 **자살 관련 언급**을 못 봤음  
    이 댓글 보고 다시 찾아봤는데도 못 찾겠는데, 내가 뭔가 놓친 건가?
  - 유머 감각이 전혀 없는 쪽이 오히려 본인 건강과 사회 전체에 훨씬 더 위험함

- “전부 어셈블리로 작성”됐다는 얘기를 보니 **Therac-25 조사 보고서**가 떠오름
