2026.07.02, 23:48
당신은 나와 단 4시간만 함께할 수 있고, 다시는 나를 보지 못하는 선생님입니다. 당신의 유일한 목표는 시간이 끝나기 전에 내가 [nasm으로 컴파일되는 64비트 인텔문법 어셈블리]을 실제로 사용할 수 있게 만드는 것입니다. 실용성이 없는 이론은 주지 마세요. 단순한 목록도 주지 마세요. 내게 알려주세요. 무엇을 가장 먼저 배워야 하는지, 무엇은 완전히 무시해야 하는지, 그리고 단 한 번만 해도 몇 달 동안 공부한 사람들 70%보다 앞서게 만드는 연습은 무엇인지
10:55 PM
유나님, 4시간이면 충분합니다. 대신 딴 길로 새지 않는다는 조건에서요.
가장 먼저, 그리고 유일하게 외워야 할 것은 System V AMD64 호출 규약입니다. 함수 인자는 순서대로 rdi, rsi, rdx, rcx, r8, r9에 담기고, 반환값은 rax에 담깁니다. 이것 하나만 몸에 배면 ft_strlen이든 ft_strcpy든 뭘 구현하든 “지금 이 레지스터에 뭐가 들어와야 하지”가 자동으로 떠오릅니다. syscall을 직접 부를 때는 이 규약이 살짝 바뀝니다 — rcx 대신 r10을 씁니다, 그리고 rax에는 인자가 아니라 syscall 번호가 들어갑니다. ft_write, ft_read를 만들 때 헷갈리는 지점이 바로 여기니까, “함수 호출용 rcx”와 “syscall용 r10″을 지금 확실히 분리해두세요.
완전히 무시해도 됩니다: SSE/AVX 같은 벡터 명령어, 부동소수점, 세그먼트 레지스터, 보호모드/리얼모드 이론, 그리고 NASM 매크로 시스템. libasm 여섯 개 함수엔 하나도 안 씁니다. mov, add, sub, cmp, jmp 계열 조건분기(je, jne, jl 등), inc/dec, 그리고 [rdi], [rdi+rax], byte [rdi] 정도의 메모리 주소 지정 방식만 있으면 여섯 함수 전부 끝납니다. “이 명령어도 배워야 하나?” 싶은 게 나오면 십중팔구 안 배워도 됩니다.
단 한 번으로 몇 달 공부한 사람 70%를 앞서는 연습은 이겁니다: ft_strlen을 지금 이 자리에서, 아무 참고 없이, 손으로 레지스터 상태를 종이에 추적하면서 짜보세요. rdi에 문자열 포인터가 들어온다, rax를 0으로 초기화한다, [rdi+rax] 바이트를 읽어서 0인지 비교한다, 아니면 rax를 증가시키고 루프, 0이면 rax를 반환한다 — 이 흐름을 코드로 옮기기 전에 레지스터 값이 매 루프마다 어떻게 바뀌는지 손으로 세 번 트레이싱하세요. 대부분의 사람은 어셈블리를 “C를 번역하는 언어”로 생각하고 코드부터 씁니다. 그러면 디버깅 지옥에 빠집니다. 반대로 레지스터 상태를 먼저 손으로 추적하는 사람은 애초에 버그를 안 만듭니다. 이게 진짜 격차입니다.
이제 ft_strlen부터 시작해볼까요? 유나님이 먼저 레지스터 흐름을 말로 설명해보시고, 제가 바로 잡아드리겠습니다.
다음에서 왜 rax를 쓰는거야? rdi는 함수 인자니까 쓰는 거 ok. 근데 왜 다른 변수 말고 rax인 게 아니라 반환값이라 rax구나. 맞아?
네, 정확히 맞습니다. (생략)
rdi에 인자로 char *str이 들어옴
rax를 0으로 초기화 (int i 를 리턴할 예정이니 편의상 반환값에 쓰는 레지스터 rax를 사용)
레이블: [rdi+rax]가 0이면 rax리턴
++rax
goto 레이블
return rax
11:17 PM
논리 흐름 자체는 정확합니다. 다만 이걸 실제 NASM 코드로 옮기기 전에, 지금 단계에서 이미 보이는 문제점들을 짚고 넘어가겠습니다.
항목 문제 근거 메모리 비교 크기 [rdi+rax]가 0이면을 코드로 옮길 때cmp byte [rdi+rax], 0처럼 크기 지정자(byte)를 반드시 명시해야 함NASM은 메모리 피연산자와 즉시값을 비교할 때 피연산자 크기를 유추할 수 없어 cmp [rdi+rax], 0만 쓰면 어셈블 에러가 남rax 초기화 방식 “rax를 0으로 초기화”를 mov rax, 0으로 쓸 계획이라면 비효율적xor rax, rax가 같은 결과를 더 짧은 바이트코드로 냄. 관례상 레지스터 0 초기화는 xor을 씀함수 진입/종료 스택 프레임(push rbp / mov rbp, rsp) 언급이 없음 ft_strlen은 리프 함수(다른 함수를 호출하지 않음)이므로 스택 프레임이 아예 필요 없음. 오히려 안 만드는 게 맞음 — 이 판단 자체는 맞게 하신 것 라벨 스코프 “레이블”이라고만 했는데 실제 코드에서 지역 라벨( .loop:)로 쓸지 전역 라벨로 쓸지 결정 안 됨함수 하나짜리 파일에서도 .loop:처럼 점(.)으로 시작하는 지역 라벨 관례를 들이는 게 나중에 6개 함수를 한 파일에 모을 때 이름 충돌을 막음반환 return rax를ret으로 쓸 계획인지 확인 필요확인 필요: System V 규약상 rax에 값이 이미 들어있으면 ret만 실행하면 됩니다. 별도 mov 불필요보완 권고
cmp앞에byte크기 지정자를 붙이세요.mov rax, 0대신xor rax, rax로 바꾸세요.- 라벨은
.loop:처럼 점으로 시작하는 지역 라벨을 쓰세요.이제 이 다섯 가지를 반영해서, 위 의사코드를 실제 NASM 문법으로 직접 써보세요.
global ft_strlen,section .text까지 포함해서요. 제가 한 줄씩 검토하겠습니다.
나 asm 짜본 적 한번도 없는데ㅜㅜㅜㅜㅜ
너무 막막하도다
하지만 해볼게?
틀리면 알려주겠지 모.
section .text
global ft_strlen
ft_strlen:
xor rax, rax
.loop:
cmp byte [rdi+rax], 0
je break
inc rax
jne .loop
.break:
ret
이걸 진짜 알려주네;;;
항목 문제 근거 je break정의되지 않은 라벨 break를 참조함실제 정의된 라벨은 .break:(점 포함, ft_strlen 스코프의 지역 라벨)인데 점 없이break라고 쓰면 NASM은 이를 별개의 전역 라벨로 인식하고, 해당 이름이 어디에도 정의되어 있지 않으므로 어셈블 에러가 발생함jne .loop의도한 “무조건 루프로 복귀”가 아니라 inc rax가 남긴 ZF 플래그에 의존하는 조건 분기가 됨cmp다음에inc rax가 실행되면서 CMP의 플래그는 이미 덮어써짐.jne는 그 직전inc rax의 결과가 0이 아닐 때만 점프하는데,inc가 0을 만드는 경우는 rax가 0xFFFFFFFFFFFFFFFF에서 오버플로될 때뿐임. 즉 현재 코드는 “문자열이 끝났는지”와 무관한 조건으로 루프를 돌고 있고, 우연히 정상 동작하는 것처럼 보일 뿐 논리적으로는 틀림. 의도는 조건 없는 복귀이므로jmp가 맞음보완 권고
je break→je .break로 수정하세요. (지역 라벨은 점을 빠뜨리면 안 됩니다.)jne .loop→jmp .loop로 수정하세요. cmp의 판단은 이미je .break에서 끝났고, 그 뒤는 무조건 복귀입니다.수정하면 이렇게 됩니다: (생략)
이 두 가지 실수 — 점 빠뜨린 라벨 참조, jmp 자리에 조건 분기 재사용 — 는 어셈블리 초보자가 가장 흔하게 저지르는 실수 유형입니다. 지금 잡아두면 나머지 다섯 함수에서 똑같은 패턴을 반복하지 않게 됩니다.
이제 이 코드를 WSL2에서 직접 어셈블·링크해서 실행까지 해보시겠습니까? 아니면 바로 ft_strcpy로 넘어가시겠습니까?
고친 코드
section .text
global ft_strlen
ft_strlen:
xor rax, rax
.loop:
cmp byte [rdi+rax], 0
je .break
inc rax
jmp .loop
.break:
ret
(내 pc에서 내가 직접) (ubuntu에서 실행. wsl 아님) (-no-pie 플래그 금지) (ar rcs libasm.a objs/ft_strlen.o 실행한 뒤 테스트 방법 알려줘) 어셈블 링크해서 실행해보자
괄호1: 안 붙이니까 자기가 실행하더라구..
(생략)
2. 테스트용 C 하네스 작성
ft_strlen이 어셈블리로만 있고 검증 기준이 없으면 신뢰할 수 없습니다. libc의strlen과 대조하는 방식이 가장 확실합니다.// test_strlen.c #include <stdio.h> #include <string.h> extern unsigned long ft_strlen(const char *str); int main(void) { char *tests[] = {"", "a", "hello", "42 gyeongsan", "yuna"}; int fail = 0; for (int i = 0; i < 5; i++) { unsigned long mine = ft_strlen(tests[i]); unsigned long libc = strlen(tests[i]); printf("\"%s\" -> mine=%lu libc=%lu %s\n", tests[i], mine, libc, mine == libc ? "OK" : "FAIL"); if (mine != libc) fail = 1; } return fail; }
항목 근거 반환 타입을 unsigned long로 선언rax 64비트 전체를 그대로 받기 위함. int로 선언하면 상위 32비트가 잘려 큰 문자열에서 오검증 발생 가능빈 문자열 ""포함루프 진입 즉시 종료 조건을 만족하는 경계값 — 어셈블리 초기 구현에서 가장 자주 놓치는 케이스 3. 컴파일·링크
bash
gcc -Wall -Wextra -c test_strlen.c -o test_strlen.o gcc test_strlen.o libasm.a -o test_strlen
-no-pie없이도 문제없습니다.ft_strlen.asm이 절대주소를 쓰지 않고 rdi 상대 접근만 하므로 PIE 바이너리에 링크되어도 정상 동작합니다.4. 실행
bash
./test_strlen전부
OK가 나오면 통과입니다. 하나라도FAIL이면 어느 문자열에서 틀렸는지, 특히 빈 문자열이나 긴 문자열 쪽을 먼저 의심하세요.실행 결과 알려주시면, 나오는 값을 보고 다음 단계(ft_strcpy) 넘어갈지 여기서 더 검증할지 판단하겠습니다.
7/3, 00:35, 잠시 쉬는시간.. 집중력의 한계가 느껴진다
그리고 마침내 7/3, 02:00, 다시 앉다.
(다음 편에 이어서…)
답글 남기기