[ASM] 42 Libasm 과제를 해보아요~(6) ft_strcmp


2026.07.04, 11:57


다음 함수 진행하고 싶습니다.

ft_strcmp 핵심 개념

ft_strlen, ft_strcpy와 달리 이번엔 반환값 자체가 정보를 담습니다. 표준 strcmp(s1, s2)는 단순히 같다/다르다(1/0)를 반환하는 게 아니라, 다른 지점에서 두 문자열의 문자 값 차이를 반환합니다. 보통 s1[i] - s2[i]로 구현합니다.

케이스반환값 예시근거
완전히 같음0모든 바이트가 같고 둘 다 같은 위치에서 \0을 만남
s1 > s2양수다른 첫 바이트에서 s1 쪽 문자 코드가 더 큼
s1 < s2음수다른 첫 바이트에서 s1 쪽 문자 코드가 더 작음

루프 종료 조건이 이전 두 함수와 다릅니다. ft_strlen, ft_strcpy는 “한쪽 문자열이 끝나면” 종료했지만, ft_strcmp는 “두 문자가 다르거나, 어느 한쪽이라도 \0이면” 종료해야 합니다. 이 조건을 놓치면 두 문자열이 완전히 같을 때 무한루프에 빠지거나, \0을 넘어서 메모리를 읽는 버그가 생깁니다.

질문: 루프 안에서 매 반복마다 검사해야 할 조건이 정확히 몇 가지이고, 각각 무엇입니까? (여기에 정확히 답한 뒤 NASM 코드를 작성해보세요.)

3가지입니다.
a[i] == 0
b[i] == 0
a[i] != b[i]

항목문제근거
a[i] == 0, b[i] == 0 두 조건 모두 필요하다고 판단논리적으로는 맞지만 최적화 관점에서 중복a[i] != b[i]를 먼저 검사해서 통과했다는 것은 이미 a[i] == b[i]가 확정된 상태입니다. 이 상태에서 a[i] == 0을 검사해 참이면, b[i]도 자동으로 0입니다(둘이 같으니까). 따라서 b[i] == 0을 별도로 검사할 필요가 없습니다. 즉 실질적으로 필요한 검사는 2가지입니다: (1) 두 바이트가 다른가, (2) (같다면) 그 값이 0인가

정정된 종료 조건

  1. a[i] != b[i] → 다르면 즉시 종료, 반환값은 a[i] - b[i]
  2. (같다면) a[i] == 0 → 둘 다 끝났다는 뜻이므로 종료, 반환값은 0
  3. 둘 다 아니면 인덱스 증가 후 반복

이 순서로 검사 개수를 3개에서 2개로 줄일 수 있다는 점을 반영해서 NASM 코드를 작성해보세요. 인자는 rdi = s1, rsi = s2이고, 반환값은 eax(32비트, int 반환이므로) 또는 관례상 movsx로 부호 확장한 rax를 씁니다 — 이 부분은 직접 코드 짜면서 판단해보시기 바랍니다.

이렇게 쓰면 왜 안돼?

  1. cmp [rdi+r8], [rsi+r8]
  2. cmp [rdi+r8], 0
  3. sub [rdi+r8], [rsi+r8]

4:28 PM

***님, 이 세 줄 모두 x86-64 명령어 인코딩 규칙 위반입니다.

항목문제근거
1. cmp [rdi+r8], [rsi+r8]오퍼랜드 둘 다 메모리x86 명령어는 ModRM 바이트로 인코딩되는데, 이 구조상 피연산자 중 하나는 반드시 레지스터 또는 즉치값이어야 함. 메모리-메모리 연산은 인코딩 자체가 존재하지 않음 (예외: MOVSB/CMPSB 같은 문자열 명령어만 RSI/RDI를 암묵적으로 사용해서 가능)
2. cmp [rdi+r8], 0크기 미지정메모리 오퍼랜드 + 즉치값 조합에서 NASM은 비교할 크기(byte/word/dword/qword)를 추론할 수 없음 → “operation size not specified” 에러. cmp byte [rdi+r8], 0 처럼 명시 필요
3. sub [rdi+r8], [rsi+r8]오퍼랜드 둘 다 메모리1번과 동일한 이유

보완 권고

  1. 두 메모리 값을 비교/연산하려면 한쪽을 먼저 레지스터에 로드해야 합니다.

nasm

   mov al, [rdi+r8]
   cmp al, [rsi+r8]
  1. 즉치값과 비교 시 항상 크기 지정자를 붙이세요. (ft_strcmp는 보통 byte 단위 비교이므로 byte 사용)
  2. sub도 동일하게 레지스터 경유가 필요합니다.

nasm

   mov al, [rdi+r8]
   sub al, [rsi+r8]

ft_strcmp 구현하실 때 이 패턴(로드 → 비교/연산)을 루프 안에서 반복하시게 될 겁니다.

section .text
global ft_strcmp

ft_strcmp:
	xor r8, r8
	.loop:
		mov r9, [rdi+r8]
		cmp r9, [rsi+r8]
		jne .calc
		cmp byte r9, 0
		je .calc
		inc r8
		jmp .loop
	.calc:
		mov rax, r9
		sub rax, [rsi+r8]
	ret
// test_strcmp.c

#include <stdio.h>
#include <string.h>

extern int ft_strcmp(const char *s1, const char *s2);

int test_cmp(char *func_name, int (*func)(const char *, const char *), const char *s1, const char *s2);

int main(void)
{
	char *s1[] = {"", "a", "abc", "mat", "cat", "hello"};
	char *s2[] = {"", "a", "abc", "map", "rat", "helloWorld"};
	int i = 0;

	int mine;
	int libc;

	int ret_cmp;
	int fail = 0;

	for (; i < 6; ++i)
	{
		printf("s1: %s, s2: %s\n", s1[i], s2[i]);
		mine = test_cmp("ft_strcmp", ft_strcmp, s1[i], s2[i]);
		libc = test_cmp("   strcmp", strcmp, s1[i], s2[i]);
		ret_cmp = mine == libc;
		printf(ret_cmp ? "OK" : "FAIL");
		if (!ret_cmp) fail = 1;
		printf("\n\n");
	}
	return (fail);
}

int test_cmp(char *func_name, int (*func)(const char *, const char *), const char *s1, const char *s2)
{
	int ret;

	ret = func(s1, s2);
	printf("%s(%s, %s) -> %d\n", func_name, s1, s2, ret);

	return (ret);
}

ft_strcmp(“mat”, “map”), ft_strcmp(“hello”, “helloWorld”)의 결과가 strcmp와 다르다.

디버깅을 해보자

mat, map에서 m 부분에서 갑자기 .break로 이동하더라.

디버깅하는 방법 알려달랬더니 냅다 코드리뷰 당해버렸다.

코드 리뷰: ft_strcmp

항목문제근거
mov r9, [rdi+r8]8바이트(qword) 단위로 메모리를 읽고 있음. 문자열 비교는 1바이트(char) 단위여야 함r9는 64비트 레지스터이므로 니모닉 크기가 자동으로 8바이트로 결정됨. s1[i] 한 글자가 아니라 s1[i]~s1[i+7] 8글자를 한 번에 로드하게 됨
cmp byte r9, 0NASM 어셈블 자체가 실패함 (문법 오류)size directive(byte/word/dword/qword)는 메모리 오퍼랜드에만 붙일 수 있음. 레지스터는 이름 자체가 크기를 결정하므로 byte r9는 상충되는 크기 지정으로 처리되어 컴파일 에러 발생
cmp r9, [rsi+r8] 분기 로직8바이트 단위 비교로는 “첫 번째로 다른 문자”의 정확한 위치를 특정할 수 없음두 8바이트 블록이 다르다고 판단해 .calc로 가더라도, 그 8바이트 중 몇 번째 바이트에서 실제로 값이 갈렸는지, 혹은 그 사이에 널 종료 문자가 끼어있는지 알 수 없음
mov rax, r9 / sub rax, [rsi+r8]반환값이 “8바이트 정수끼리의 차”가 되어 표준 strcmp 규약과 어긋남libasm/libft 표준은 통상 (unsigned char)s1[i] - (unsigned char)s2[i] 형태의 단일 바이트 차이를 요구함
메모리 오버런 위험짧은 문자열에서 8바이트를 한 번에 읽으면 문자열 경계 밖의 매핑되지 않은 페이지를 건드릴 수 있음예: 길이 3인 문자열이 페이지 끝에 위치하면 나머지 5바이트를 읽는 순간 세그폴트 가능

보완 권고

  1. movzx + byte 레지스터로 1바이트씩 로드하도록 수정 (r9b, r10b 사용, 상위 비트는 movzx로 제로 확장)
  2. cmp byte r9, 0test r9, r9 로 대체 (이미 movzx로 제로 확장된 상태이므로 전체 레지스터 test로 0 여부 판단 가능)
  3. 8바이트 단위 비교 로직 전면 제거, 바이트 단위 비교로 재작성

수정안:

asm

section .text
global ft_strcmp

ft_strcmp:
	xor r8, r8
	.loop:
		movzx r9, byte [rdi+r8]
		movzx r10, byte [rsi+r8]
		cmp r9, r10
		jne .calc
		test r9, r9
		je .calc
		inc r8
		jmp .loop
	.calc:
		mov rax, r9
		sub rax, r10
	ret

로컬 레이블(.loop, .calc) 사용 방식은 문제없습니다. GDB에서 확인할 때는 이전에 안내드린 대로 break ft_strcmp.calc로 접근 가능합니다.

movzx (Move with Zero-Extend)는 작은 크기의 값을 더 큰 레지스터로 옮기면서 남는 상위 비트를 0으로 채우는 명령어입니다.

  • movzx r9, byte [rdi+r8] → 메모리에서 1바이트만 읽어와 r9(64비트)에 넣고, 나머지 상위 7바이트는 전부 0으로 채웁니다.
  • 이게 필요한 이유: 만약 그냥 mov r9b, [rdi+r8]로 하위 8비트만 채우면 r9의 상위 56비트는 이전에 남아있던 쓰레기값이 그대로 남습니다. 그 상태에서 r9 전체(64비트)로 cmptest를 하면 상위 비트의 쓰레기값 때문에 잘못된 비교 결과가 나올 수 있습니다.
  • movzx는 이 문제를 원천 차단해서, 항상 “순수한 바이트 값”만 레지스터에 들어있도록 보장합니다.

참고로 부호 있는 값(예: signed char)을 확장할 때는 상위 비트를 부호에 맞춰 채우는 movsx(sign-extend)를 씁니다. ft_strcmp처럼 unsigned char 기준 비교를 요구하는 경우엔 movzx가 맞습니다.

test는 두 오퍼랜드를 AND 연산하되 결과를 저장하지 않고 플래그 레지스터만 갱신하는 명령어입니다.

  • test r9, r9r9 AND r9를 계산해 ZF(Zero Flag) 등을 설정하지만, r9 값 자체는 그대로 유지합니다.
  • r9가 0이면 r9 AND r9 = 0이 되어 ZF가 1로 설정되고, 이어지는 je(jump if equal, 즉 ZF=1일 때 점프)가 동작합니다.
  • test r9, r9 + je는 “r9가 0인지 검사해서 0이면 점프”하는 관용적 패턴이며, cmp r9, 0보다 명령어가 짧고 실행 속도가 미세하게 빠릅니다.

je는 zf가 1일때 동작하나요? cmp 후 무엇이 설정되어야 je가 동작하나요?

네, 맞습니다. je(Jump if Equal)는 ZF(Zero Flag)가 1일 때 동작합니다. 사실 jejz(Jump if Zero)와 완전히 동일한 명령어의 다른 니모닉일 뿐이며, 둘 다 내부적으로 같은 opcode를 사용합니다.

cmp a, b는 내부적으로 a - b를 계산하되 결과는 버리고 플래그만 설정합니다. 이때 설정되는 주요 플래그는:

  • ZF (Zero Flag): a - b == 0, 즉 a == b일 때 1로 설정 → je/jz가 이 플래그를 보고 점프
  • SF (Sign Flag): 결과가 음수일 때 1
  • CF (Carry Flag): 부호 없는 뺄셈에서 빌림(borrow) 발생 시 1 → jb, ja 등이 사용
  • OF (Overflow Flag): 부호 있는 연산에서 오버플로 발생 시 1 → jg, jl 등이 사용

cmp r9, r10 다음에 je를 쓰면 “r9와 r10이 같으면 점프”가 되고, 이건 test r9, r9 다음에 쓴 je(“r9가 0이면 점프”)와는 검사 대상이 다르지만 둘 다 ZF=1 여부를 확인한다는 메커니즘은 동일합니다.

asm 코드와 test 코드입니다. 검토 부탁드립니다.

section .text
global ft_strcmp

ft_strcmp:
	xor r8, r8
	.loop:
		movzx r9, byte [rdi+r8]
		movzx r10, byte [rsi+r8]
		cmp r9, r10
		jne .calc
		test r9, r9
		je .calc
		inc r8
		jmp .loop
	.calc:
		mov rax, r9
		sub rax, r10
	ret

어셈블리 코드 (ft_strcmp.asm)

항목문제근거
없음인덱스에 r8, 임시값에 r9/r10을 사용해 rax를 아예 건드리지 않다가 마지막에만 반환값을 채우는 구조입니다. 이전에 논의했던 “인덱스로 rax를 쓰다가 마지막에 덮어쓰기” 방식보다 실수 여지가 적은 설계입니다. movzx로 8비트를 64비트로 제로 확장한 뒤 그대로 sub한 것도 유효합니다 — 이후 mov rax, r9 / sub rax, r10 계산 결과가 truncation 없이 정확합니다

트레이싱 검증 ("hello" vs "helloWorld", index=5): r9=0(s1 종료), r10='W'=87. cmp r9,r10에서 이미 다르므로 jne .calc로 분기 — test r9,r9 경로를 거치지 않아도 동일한 값(0-87=-87)이 계산되어 두 종료 경로가 결과적으로 일치합니다. 로직 결함 없음.

테스트 코드 (test_strcmp.c)

항목문제근거
없음일반 케이스 6종(빈 문자열, 완전 일치, 중간 문자 차이, 첫 문자 차이, prefix 관계의 길이 차)과 NULL 케이스 3종(첫 인자 NULL, 둘째 인자 NULL, 둘 다 NULL) 조합이 누락 없이 구성되어 있습니다
(선택) 최종 요약 라인 부재test_strcpy.c에서 썼던 === OK/FAIL === 형태의 전체 요약 출력이 없음기능상 문제는 아니고, 각 항목별 OK/FAIL은 이미 출력되나 스크롤이 길어질 경우 한눈에 전체 결과를 확인하기 어려움

보완 권고

  1. (선택) main 마지막에 printf("=== %s ===\n", fail ? "FAIL" : "OK");를 추가해 이전 테스트 파일과 형식을 통일하세요.

로직 결함은 없으므로 바로 빌드·실행하셔도 됩니다.

항목문제근거
8비트 초과 값 비교 미검증바이트 값이 0x80 이상(예: 128~255 범위, 이른바 “음수처럼 보이는” 문자)인 케이스가 테스트에 없음POSIX 표준상 strcmp은 문자를 unsigned char로 해석하여 비교해야 합니다. ft_strcmp.asm에서 movzx(zero-extend)를 쓴 것은 이 규격에 맞는 정확한 선택이지만, 이걸 증명하는 테스트 케이스가 없습니다. 만약 나중에 실수로 movsx(sign-extend)를 썼다면 0x80 이상 바이트에서 결과가 반대로 뒤집히는데, 지금 테스트 세트(전부 ASCII 알파벳)로는 이 버그를 절대 못 잡습니다
완전 동일하지만 긴 문자열 부재가장 긴 케이스가 "helloWorld"(10자)뿐이고, 수십~수백 바이트급 긴 문자열에서 두 문자열이 끝까지 동일한 케이스가 없음루프가 반복적으로 잘 도는지, 인덱스(r8) 오버플로우나 예상치 못한 조기 종료가 없는지는 문자열 길이가 짧으면 우연히 통과할 수 있음
마지막 한 글자만 다른 케이스 부재예: "hello" vs "hellp"처럼 문자열 끝부분(널 문자 직전)에서만 값이 갈리는 경우가 없음종료조건 두 가지(다름/둘다 0)의 경합이 문자열 맨 끝에서 정확히 처리되는지 확인하는 경계값 테스트
dest/src 버퍼 재사용 없음(해당 사항 없음 — ft_strcmp은 읽기 전용 함수라 버퍼 문제 없음)

보완 권고

  1. s1/s2 배열에 0x80 이상 바이트를 포함한 문자열 쌍을 추가하세요. 예: "\x80\x01" vs "\x7f\x01"ft_strcmp이 양수를 반환해야 정상(0x80 > 0x7f as unsigned char)이고, 만약 movsx를 잘못 썼다면 부호가 반대로 나와 버그가 드러납니다.
  2. 30~50바이트 이상의 동일한 긴 문자열 쌍을 추가하세요.
  3. 마지막 한 글자만 다른 동일 길이 문자열 쌍("hello" vs "hellp")을 추가하세요.

1번이 가장 중요합니다 — 지금 어셈블리 구현 자체는 맞게 되어 있지만, 이 특성을 검증하는 테스트가 없으면 나중에 코드를 고치다 실수로 부호 확장(movsx)으로 바꿔도 테스트가 계속 통과할 위험이 있습니다.

좋습니다, ft_strcmp는 검증 완료로 처리하겠습니다.

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다