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인가정정된 종료 조건
a[i] != b[i]→ 다르면 즉시 종료, 반환값은a[i] - b[i]- (같다면)
a[i] == 0→ 둘 다 끝났다는 뜻이므로 종료, 반환값은 0- 둘 다 아니면 인덱스 증가 후 반복
이 순서로 검사 개수를 3개에서 2개로 줄일 수 있다는 점을 반영해서 NASM 코드를 작성해보세요. 인자는
rdi = s1,rsi = s2이고, 반환값은eax(32비트,int반환이므로) 또는 관례상movsx로 부호 확장한rax를 씁니다 — 이 부분은 직접 코드 짜면서 판단해보시기 바랍니다.
이렇게 쓰면 왜 안돼?
- cmp [rdi+r8], [rsi+r8]
- cmp [rdi+r8], 0
- 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번과 동일한 이유 보완 권고
- 두 메모리 값을 비교/연산하려면 한쪽을 먼저 레지스터에 로드해야 합니다.
nasm
mov al, [rdi+r8] cmp al, [rsi+r8]
- 즉치값과 비교 시 항상 크기 지정자를 붙이세요. (
ft_strcmp는 보통 byte 단위 비교이므로byte사용)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바이트를 읽는 순간 세그폴트 가능 보완 권고
movzx+ byte 레지스터로 1바이트씩 로드하도록 수정 (r9b,r10b사용, 상위 비트는movzx로 제로 확장)cmp byte r9, 0→test r9, r9로 대체 (이미 movzx로 제로 확장된 상태이므로 전체 레지스터 test로 0 여부 판단 가능)- 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비트)로cmp나test를 하면 상위 비트의 쓰레기값 때문에 잘못된 비교 결과가 나올 수 있습니다.movzx는 이 문제를 원천 차단해서, 항상 “순수한 바이트 값”만 레지스터에 들어있도록 보장합니다.참고로 부호 있는 값(예: signed char)을 확장할 때는 상위 비트를 부호에 맞춰 채우는
movsx(sign-extend)를 씁니다.ft_strcmp처럼unsigned char기준 비교를 요구하는 경우엔movzx가 맞습니다.
test는 두 오퍼랜드를 AND 연산하되 결과를 저장하지 않고 플래그 레지스터만 갱신하는 명령어입니다.
test r9, r9는r9 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일 때 동작합니다. 사실je는jz(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은 이미 출력되나 스크롤이 길어질 경우 한눈에 전체 결과를 확인하기 어려움 보완 권고
- (선택)
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은 읽기 전용 함수라 버퍼 문제 없음) — 보완 권고
s1/s2배열에 0x80 이상 바이트를 포함한 문자열 쌍을 추가하세요. 예:"\x80\x01"vs"\x7f\x01"—ft_strcmp이 양수를 반환해야 정상(0x80 > 0x7f as unsigned char)이고, 만약movsx를 잘못 썼다면 부호가 반대로 나와 버그가 드러납니다.- 30~50바이트 이상의 동일한 긴 문자열 쌍을 추가하세요.
- 마지막 한 글자만 다른 동일 길이 문자열 쌍(
"hello"vs"hellp")을 추가하세요.1번이 가장 중요합니다 — 지금 어셈블리 구현 자체는 맞게 되어 있지만, 이 특성을 검증하는 테스트가 없으면 나중에 코드를 고치다 실수로 부호 확장(
movsx)으로 바꿔도 테스트가 계속 통과할 위험이 있습니다.
좋습니다, ft_strcmp는 검증 완료로 처리하겠습니다.
답글 남기기