我有以下代码:
#include <sys/mman.h>
#include <unistd.h>
#include <cstdio>
int main() {
char *p = (char *)mmap(0, 0x3000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
munmap(p + 0x2000, 0x1000);
p += 0x800;
printf("%zd\n", read(0, p, 0x3000));
return 0;
}
当编译并使用将写入可写内存的输入运行它时,根据输入该输入的方式,我会得到不同的行为:
$ python3 -c 'print("A"*0x3000)' | ./test
4096
$ python3 -c 'print("A"*0x3000)' > input.bin; ./test < input.bin
6144
$ nc.traditional -l -p 1337 -e ./test &
[1] 25855
$ python3 -c 'print("A"*0x3000)' | nc localhost 1337
-1
[1]+ Done nc.traditional -l -p 1337 -e ./test
在所有情况下,我都希望read()调用返回相同的结果,但事实并非如此。为什么我会有不同的行为?
答案 0 :(得分:0)
此命令的strace输出显示,一次写入了全部12288(0x3000)字节:
$ strace python3 -c 'print("A"*0x3000)' | ./test
...
write(1, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 12288) = 12288
...
在管道另一端的./test
命令上使用strace显示仅读取4096(0x1000)个字节:
$ python3 -c 'print("A"*0x3000)' | strace ./test
...
read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 12288) = 4096
...
如果./test
的标准输入来自input.bin
(包含12288个'A',后跟换行符),则strace显示读取了6144(0x1800)个字节:
$ strace ./test < input.bin
...
read(0, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"..., 12288) = 6144
...
我认为只能通过在“ fs / pipe.c”中的pipe_read
函数中研究内核代码以从管道读取来找到解释。在内部,管道包含struct pipe_buffer
的循环数组,每个循环数组都包含一个指向数据页的指针。 pipe_read
函数遍历这些缓冲区,将内容复制到用户,直到读取了所有请求的数据,管道中没有任何内容(在阻塞模式下等待来自写入器的更多数据之后)或错误将数据复制到用户时发生。发生故障时,仅返回4096个字节而不是6144个字节的原因似乎是由于以下代码:
written = copy_page_to_iter(buf->page, buf->offset, chars, to);
if (unlikely(written < chars)) {
if (!ret)
ret = -EFAULT;
break;
}
ret += chars;
buf->offset += chars;
buf->len -= chars;
(以上部分来自Linux内核版本4.19。)
在这里,chars
是要从当前管道缓冲区复制的数量,written
是实际复制的数量,ret
是函数的返回值,通常是读取的字节数。 ret
的初始值为0。通常将written
设置为chars
,除非发生地址错误,在这种情况下它将小于chars
。
从管道复制第二页时发生错误。成功复制了第一页,因此ret
将为4096。复制第二页时,6144-4096 = 2048(0x800)字节后,用户缓冲区中发生地址错误,因此break;
语句为在到达ret += chars;
语句之前到达循环中断。 ret
未设置为-EFAULT
,因为它非零。但是,就pipe_buf
函数的返回值而言,从管道第二页到用户缓冲区的2048字节的部分副本已被忽略。 (忽略的数据不会从管道中丢弃。管道中的位置仅在成功复制后才继续移动,因此对pipe_read
的后续调用将从同一点继续。)
请注意,尽管read
仅返回4096,但实际上似乎实际上已将6144字节复制到用户缓冲区。通过p + 4096
调用返回后,从read
开始检查缓冲区内容,可以确认这一点。
我敢肯定pipe_read
函数的相关部分可以重写以正确说明部分复制的部分,但是我不知道核心内核开发人员是否会认为它值得修复。例如,一个修复程序可能会将上面的代码替换为以下(未经测试的)代码:
written = copy_page_to_iter(buf->page, buf->offset, chars, to);
ret += written;
buf->offset += written;
buf->len -= written;
if (unlikely(written < chars)) {
if (!ret)
ret = -EFAULT;
break;
}
我非常确定,通过更改内核代码,您的./test
程序将能够从管道读取6144字节。