2026.08.16, 18:46
아무도 안 궁금했던 근황: 보조강사 알바, 8/7 정보처리기사 필기 시험, 8/9 독학사 컴공 3단계 시험, 포트폴리오 만들어서 정규직으로 써달라고 메일 보내기
(바빴다는 뜻)
아무튼 ft_read를 만들어볼거예요~~
ft_read입니다
ft_read는 ft_write와 거의 대칭이지만, 함정이 하나 다릅니다.
구분 fd buf count syscall 번호 ft_write rdi rsi rdx 1 ft_read rdi rsi rdx 0 syscall 번호와 에러 처리(
push rax→call __errno_location wrt ..plt→pop 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()으로 임시 파일 열고 알려진 내용을 미리 writeft_read(fd, buf, size)호출 → 실제 libcread()와 리턴값·버퍼 내용 비교- 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;
}
}