[ASM] 42 Libasm 과제를 해보아요~(9) ft_read


2026.08.16, 18:46


아무도 안 궁금했던 근황: 보조강사 알바, 8/7 정보처리기사 필기 시험, 8/9 독학사 컴공 3단계 시험, 포트폴리오 만들어서 정규직으로 써달라고 메일 보내기

(바빴다는 뜻)

아무튼 ft_read를 만들어볼거예요~~

ft_read입니다

ft_read는 ft_write와 거의 대칭이지만, 함정이 하나 다릅니다.

구분fdbufcountsyscall 번호
ft_writerdirsirdx1
ft_readrdirsirdx0

syscall 번호와 에러 처리(push raxcall __errno_location wrt ..pltpop qword [rax]mov rax, -1) 구조는 ft_write에서 만든 걸 그대로 재사용할 수 있습니다.

다른 점 하나: write는 buf를 커널이 “읽기만” 하지만, read는 커널이 buf에 “쓰기”를 합니다. 이게 코드 자체에는 영향 없지만(어차피 syscall이 알아서 처리), 테스트 설계에는 영향을 줍니다 — NULL buf를 넘겼을 때 write는 EFAULT가 나는 지점이 “읽으려는 시도”였다면, read는 “쓰려는 시도”에서 EFAULT가 납니다. 결과적으로 에러 코드는 똑같이 EFAULT(14)이지만, count=0일 때는 write와 마찬가지로 커널이 buf 검사를 생략하고 0을 반환한다는 점도 동일합니다.

직접 NASM 코드를 작성해보시겠습니까? ft_write 코드를 골격으로 삼아 syscall 번호와 함수 이름만 바꾸는 방식으로 시작하시면 됩니다.

시스콜 넘버부터 찾아봤어요: sys_read는 0입니다.

mov rax, 0
syscall

아니 세상에 클로드가 번호 친절하게 알려줬는데 코앞에 두고 못 봤네;;; 이거 뭐지요??

근데 일단?? read 함수가 어떻게 동작할까?요??

write: fd(rdi)에 buf(rsi)의 내용을 count(rdx)만큼 쓸 것이다, syscall(rax) 1

read: fd(rdi)을 buf(rsi)에 count(rdx)만큼 읽을 것이다, syscall(rax) 0

이거잖아

그러니까 시스콜이 알아서 처리한 다음에, rax를 반환하는거지

rax >= 0, 쓰기 성공한 바이트
rax < 0, -errno

그러면 그냥 write 재사용하면 되는거잖아요…. 날 속였구나(아님)

그러면 ft_read를 구현하라는 게 무슨 의미가 있지? 시스콜 부를 때 rax 0 세팅이랑 테스트 코드만 다르잖아. 테스트코드 짜보라는 건가?

section .text
global ft_read

ft_read:
    mov rax, 0
    syscall

    cmp rax, 0
    jl .err
    .end:
    ret

.err:
    neg rax
    push rax
    call __errno_location wrt ..plt
    pop qword [rax]
    mov rax, -1
    jmp .end

section .note.GNU-stack noalloc noexec

write 함수 테스트는 어떻게 하는지 알겠는데 read 함수 테스트는 어떻게 해야해? 파일 열고 fd 받아서 거기서 읽어보면 되나? 안열린 fd로도 errno랑 -1 리턴 확인하고?

네, 맞습니다. 기본 방향은 맞고, 몇 가지 케이스를 더 추가하시면 완성도가 올라갑니다.

1. 정상 케이스 (파일 fd)

  • open()으로 임시 파일 열고 알려진 내용을 미리 write
  • ft_read(fd, buf, size) 호출 → 실제 libc read()와 리턴값·버퍼 내용 비교
  • size보다 파일 내용이 작을 때 (부분 읽기), size가 0일 때도 케이스 추가

2. 잘못된 fd

  • 안 연 fd (예: 임의의 큰 정수) → -1, errno == EBADF
  • 이미 close()한 fd로 read 시도 → 동일하게 -1, EBADF 확인

3. EOF 케이스

  • 파일을 끝까지 읽은 후 추가로 read → 리턴값 0 (에러 아님, errno 건드리지 않아야 함)

4. NULL buf

  • ft_write 때처럼 fork + waitpid + WIFSIGNALED로 SIGSEGV 확인
  • 단, fd는 유효한 값으로 줘야 커널이 버퍼 검증 단계까지 진입합니다 (fd가 먼저 걸리면 buf 검증 전에 EBADF로 리턴되는 케이스도 있으니, 정상 fd + NULL buf 조합으로 명확히 분리)

5. 쓰기 전용 fd로 read

  • O_WRONLY로 연 fd에 read 시도 → -1, errno == EBADF
  • write 테스트에서 읽기전용 fd로 write 했던 것과 대칭되는 케이스입니다

6. 파이프 fd (fork/pipe)

  • ft_write 테스트 인프라 재사용 가능: 파이프 write 쪽에 데이터 넣고 read 쪽 fd로 ft_read 검증
  • 특히 파이프가 비어있고 write 쪽이 열려 있을 때 read가 블로킹되는지까지 확인하려면 별도 자식 프로세스로 타이밍 제어 필요 (필수는 아니고, 시간 되시면 추가)

이 중 4번(NULL buf)과 1번(정상 케이스)이 핵심이고, 2·5번은 errno 정확성 검증 차원에서 중요합니다. 6번은 write 테스트 인프라를 그대로 재사용하실 수 있으니 시간 대비 효율이 좋습니다.

// test_read.c

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>

extern ssize_t ft_read(int fd, const void *buf, size_t count);

int test_read(char *func_name, ssize_t (*func)(int, void *, size_t), int fd, char *buf, size_t count, int *errno_ptr);
int test_null_case(char *func_name, ssize_t (*func)(int, const void *, size_t), int fd, int *errno_ptr);

int main(void)
{
	int mine;
	int libc;

	unsigned int fd;

	char buf_mine[100];
	char buf_libc[100];

	size_t count;

	int errno_mine;
	int errno_libc;

	int ret_cmp;
	int fail = 0;

	{
		fd = open("test_strlen.c", O_RDONLY);
		count = 42;
	
		mine = test_read("ft_read", ft_read, fd, buf_mine, count, &errno_mine);
		libc = test_read("   read",    read, fd, buf_libc, count, &errno_libc);
		ret_cmp = mine == libc;
		printf(ret_cmp ? "OK" : "FAIL");
		if (!ret_cmp)
			fail = 1;
		printf("\nerrno: %d %d: ", errno_mine, errno_libc);
		ret_cmp = errno_mine == errno_libc;
		printf(ret_cmp ? "OK" : "FAIL");
		if (!ret_cmp)
			fail = 1;
		printf("\n\n");
	}














	for (int fd = -1; fd <= 3; ++fd)
	{
		i = 0;
		for (; i < 9; ++i)
		{
			mine = test_read("ft_read", ft_read, fd, buf_mine, count, &errno_mine);
			libc = test_read("   read",    read, fd, buf_libc, count, &errno_libc);
			ret_cmp = mine == libc;
			printf(ret_cmp ? "OK" : "FAIL");
			if (!ret_cmp) fail = 1;
			printf("\nerrno: %d %d: ", errno_mine, errno_libc);
			ret_cmp = errno_mine == errno_libc;
			printf(ret_cmp ? "OK" : "FAIL");
			if (!ret_cmp) fail = 1;
			printf("\n\n");
		}
	
		for (; i < 10; ++i)
		{
			printf("str: %s\n", str[i]);
			mine = test_null_case("ft_read", ft_read, fd, &errno_mine);
			libc = test_null_case("   read",    read, fd, &errno_libc);
			ret_cmp = mine == libc;
			printf(ret_cmp ? "OK" : "FAIL");
			if (!ret_cmp) fail = 1;
			printf("\nerrno: %d %d: ", errno_mine, errno_libc);
			ret_cmp = errno_mine == errno_libc;
			printf(ret_cmp ? "OK" : "FAIL");
			if (!ret_cmp) fail = 1;
			printf("\n\n");
		}
	}

	printf("=== ");
	printf(fail ? "FAIL" : "OK");
	printf(" ===\n");
	write(1, "\n", 1);
	return (fail);
}

int test_read(char *func_name, ssize_t (*func)(int, void *, size_t), int fd, char *buf, size_t count, int *errno_ptr)
{
	int ret;

	ret = func(fd, buf, count);
	*errno_ptr = errno;
	printf(": %s(%d, %s, %lu) -> %d\n", func_name, fd, buf, (unsigned long)count, ret);

	return (ret);
}

int test_null_case(char *func_name, ssize_t (*func)(int, const void *, size_t), int fd, int *errno_ptr)
{
	int pipe_fd[2];
	pipe(pipe_fd);
	
	pid_t pid = fork();
	
	if (pid == 0) {
		close(pipe_fd[0]);
		int ret = test_read(func_name, func, fd, NULL, errno_ptr);
		int payload[2] = {ret, *errno_ptr};
		read(pipe_fd[1], payload, sizeof(payload));
		close(pipe_fd[1]);
		_exit(0);
	} else {
		close(pipe_fd[1]);
		int status;
		waitpid(pid, &status, 0);
	
		if (WIFSIGNALED(status)) {
			printf("%s: 시그널 %d로 종료됨 (예상: SIGPIPE==%d, 일치:%d)\n",
				func_name, WTERMSIG(status), SIGPIPE, WTERMSIG(status) == SIGPIPE);
			close(pipe_fd[0]);
			return 1;
		}
		int payload[2] = {0, 0};
		read(pipe_fd[0], payload, sizeof(payload));
		close(pipe_fd[0]);
		*errno_ptr = payload[1];
		printf("%s: 정상 종료됨, ret=%d, errno=%d\n",
			func_name, payload[0], payload[1]);
		return 0;
	}
}

코멘트

답글 남기기

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