为什么我的 Rust 中的 C strlen() 也在 print 中计算字符串切片! `s` 变量后的宏?

Why does my C strlen() in Rust also count string slice inside print! macro after `s` variable?

所以我只是在 Rust 中修补 C 库,我发现以下代码:

extern crate libc;
use libc::{c_char, c_int, size_t};


extern "C" {

    fn printf(fmt: *const c_char, ...) -> c_int;
    
    fn strlen(arr: *const c_char) -> size_t;
}

fn main() {
    unsafe {
        printf("This uses C's standard lib printf".as_ptr() as *const i8);
        print!("\n");
        let s = "Useless thing again";
        print!("Length of {}: ", s);
        let x = strlen(s.as_ptr() as *const i8);
        print!("{}", &x);
    }
}

会产生这个:

This uses C's standard lib printf

Length of Useless thing again: 31  

strlen() 也统计了 print! 宏里面的字符串切片。但如果我这样做:

extern crate libc;
use libc::{c_char, c_int, size_t};


extern "C" {

    fn printf(fmt: *const c_char, ...) -> c_int;
    
    fn strlen(arr: *const c_char) -> size_t;
}

fn main() {
    unsafe {
        printf("This uses C's standard lib printf".as_ptr() as *const i8);
        print!("\n");
        print!("blah blah blah\n");
        let s = "Useless thing again";
        let x = strlen(s.as_ptr() as *const i8);
        print!("{}", &x);
    }
}

它将产生这个:

This uses C's standard lib printf

blah blah blah
19

它正确地计算了“再次无用的东西”并且不会计算 s 变量以上的任何东西。我知道它可能与记忆有某种联系,但我实际上对低水平很陌生。可以详细解释一下吗?

这归结为 C 字符串、胖指针之间的差异,以及字符串文字在可执行文件中的存储方式。

C 字符串

您可能已经知道,C 将字符串表示为 char *。由于无法知道何时停止从内存中读取字符串,因此在末尾添加了一个空终止符(值为 0 的字节)。

所以 strlen 所做的只是计算字节数,直到找到值为 0 的字节。printf 做类似的事情,只是将找到的内容输出到标准输出。

// This string occupies 5 bytes of memory due to the implicit null terminator
char *string_literal = "test";
// ['t', 'e', 's', 't', 0]

胖指针

但是,C 字符串方法可能存在问题。如果要获取子字符串,则需要修改原始字符串以添加新的空终止符或将所需部分复制到内存的新部分。解决方法是用指针

存储字符串的长度
// This isn't technically correct, but it is easier to think of this way
pub struct string {
    ptr: *const i8,
    length: usize,
}

您可以看到 C++ std::string 和 Rust 的切片中使用的胖指针。由于 Rust 决定使用胖指针作为默认值,编译器将选择在可能的情况下不包含空终止符以保存 space.

内存

在 Linux 可执行文件(ELF 格式)中,代码中使用的所有字符串文字和常量均由编译器自行决定添加到二进制文件的文本部分。

在不知道太多的情况下,我将猜测第一个代码示例的文本部分是什么样的:

This uses C's standard lib printf[=12=]\nUseless thing againLength of : [=12=]

我通过将所有字符串文字按照它们在代码中给出的顺序放在一起并删除将在编译时删除的部分(例如 rust 的 print 语句中的 {} )得到了这个近似值.通过这种天真的估计,我们实际上看到与第一个代码示例的输出匹配的空终止符之前正好有 31 个字符。您可以使用 objdump -sj .text executable_file 自行验证(假设我的命令正确)。

例外情况

有一件事我想指出的是字符的长度是不固定的。例如,一个 Unicode 字符可以是 4 个字节长。所以如果你打算给c传递一个字符串,建议你使用二进制字符串来代替,这样可以更明确地说明数据类型,如果你不确定它是否会被传递,直接添加空终止符。

// The b converts the string to a [u8; N] and [=13=] is the null terminator.
let example = b"test 123[=13=]";