[카테고리:] 42

  • [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;
    	}
    }