Closed9

所有権の移動は実際は何をしている??

yukiyuki
#[derive(Debug)]
struct SomeData {
    name: String,
}

fn some_take_ownership(some: SomeData) {
    println!("took ownership: {:?}", &some as *const SomeData);
}

fn some_borrowing(some: &SomeData) {
    println!("borrowed: {:?}", some as *const SomeData);
}

fn main() {
    let data = SomeData {
        name: String::from("hoge"),
    };
    let former_address = &data as *const SomeData;
    println!("{:?}", &data as *const SomeData);
    some_borrowing(&data);
    some_take_ownership(data);
    unsafe {
        println!(
            "address: {:?}, value: {:?}",
            former_address,
            std::ptr::read(former_address)
        );
    }
} // ここの時点で double free するっぽい。
yukiyuki

上記コードは見るからにダメだが、コンパイルは通る。

実行結果は下記のようになる。

0x7ffeecdcdee8
borrowed: 0x7ffeecdcdee8
took ownership: 0x7ffeecdcdf70
address: 0x7ffeecdcdee8, value: SomeData { name: "hoge" }
ownership-prac(75640,0x10d0dfdc0) malloc: *** error for object 0x7fa938405cc0: pointer being freed was not allocated
ownership-prac(75640,0x10d0dfdc0) malloc: *** set a breakpoint in malloc_error_break to debug
zsh: abort      cargo run
yukiyuki

疑問点は下記かな?

  1. 0x7fa938405cc0 は一度も手動で確保した覚えはないアドレスだと思う。これは何?
  2. former_address 部分を読みに行って値を読み込めてしまうのはなぜ? address: 0x7ffeecdcdee8, value: SomeData { name: "hoge" } という標準出力は出ないのが期待値だった。
yukiyuki

一旦2から考えてみる。想定をコメントに書いてみる。

fn main() {
    let data = SomeData {
        name: String::from("hoge"),
    };
    // data のアドレスを保持させる。
    let former_address = &data as *const SomeData;
    println!("{:?}", &data as *const SomeData);
    // ここは借用なので、所有権の移動はとくに起きない。
    some_borrowing(&data);
    // この関数を呼び出した時点で、data の所有権は main から some_take_ownership に移っているはず。
    some_take_ownership(data);
    unsafe {
        println!(
            "address: {:?}, value: {:?}",
            former_address,
            // この領域はすでに解放されていると思っていたので、読み込めないのが正しいと思っていた。
            std::ptr::read(former_address)
        );
    }
}

要するに data は some_take_ownership にて所有権を奪ったあとでも参照できてしまった。some_take_ownership 関数を通ったあとでも、main 関数が引き続き所有権をもっているということ?

所有権をもっているとコンパイラが判定しているかどうかを確かめるため、少しコードを改変してみた。

#[derive(Debug)]
struct SomeData {
    name: String
}

fn some_take_ownership(some: SomeData) {
    println!("took ownership: {:?}", &some as *const SomeData);
}

fn some_borrowing(some: &SomeData) {
    println!("borrowed: {:?}", some as *const SomeData);
}

fn main() {
    let data = SomeData{ name: String::from("hoge") };
    let former_address = &data as *const SomeData;
    println!("{:?}", &data as *const SomeData);
    some_borrowing(&data);
    some_take_ownership(data);
    // data をもう一度使用するコードを safe Rust でわざと追加した。
    // これは、コンパイラ的にはすでに解放済み判定している箇所を参照しようとしているから、
    // コンパイルエラーということになる。
    println!("{:?}", &data as *const SomeData);

    unsafe {
        let ret = std::ptr::read(former_address);
        println!("address: {:?}, value: {:?}", former_address, ret);
    }
}

結果はコンパイルエラーとなり、コンパイラ側では data に対する所有権はすでにないものとして判定されていることがわかった。

yukiyuki

所有権を奪うはずの関数が、しばらくメモリに値を残し続ける…?そんなはずは…?と思ったので、some_take_ownership 関数に対して、その関数を呼び出した際にムーブされた先のアドレスを返すようにしてみた。

期待値としては、some_take_ownership 関数のブロックを抜けると、その関数内に所有権が移っていたすべての値は解放されるので、std::ptr::read しても指した先のアドレスにはなにもないといったようなエラーが出ること。

#[derive(Debug)]
struct SomeData {
    name: String,
}

fn some_take_ownership(some: SomeData) -> *const SomeData {
    // some に main 関数から data が渡されると、値の所有権がこの関数に移る。
    // なので、main 関数内の data と some_take_ownership 内の some は違うアドレスを指す。
    // それを利用して、ムーブ後のアドレスを返させ、main 関数で呼び出すことにする。
    println!("took ownership: {:?}", &some as *const SomeData);
    &some as *const SomeData
}

fn some_borrowing(some: &SomeData) {
    println!("borrowed: {:?}", some as *const SomeData);
}

fn main() {
    let data = SomeData {
        name: String::from("hoge"),
    };
    let former_address = &data as *const SomeData;
    println!("{:?}", &data as *const SomeData);
    some_borrowing(&data);
    let took = some_take_ownership(data);
    unsafe {
        println!(
            "address: {:?}, value: {:?}",
            former_address,
            std::ptr::read(former_address)
        );
        println!("took: {:?}, value: {:?}", &took, std::ptr::read(took));
    }
} // ここの時点で double free するっぽい。

実際動かしてみる。

0x7ffeebb71e50
borrowed: 0x7ffeebb71e50
took ownership: 0x7ffeebb71ee0
address: 0x7ffeebb71e50, value: SomeData { name: "hoge" }
ownership-prac(75783,0x112666dc0) malloc: *** error for object 0x7f8571405cc0: pointer being freed was not allocated
ownership-prac(75783,0x112666dc0) malloc: *** set a breakpoint in malloc_error_break to debug

"took: {:?}, value: {:?}" という文言がプリントされる前に pointer being freed was not allocated ということで、ここはきちんと解放されているということがわかる。

つまり、main 関数内だけ少し特殊なのか??

yukiyuki

あ、ちょっとミスをみつけた。former_address がおかしそう。

yukiyuki

別で実験しようとしていたものだが、下記はちゃんとコンパイルが通らない。実装の意図としては、デキる限り一時変数をなくしておこうというもの。

former_address&data as *const SomeData を束縛すると、新しいメモリ領域が確保されてしまうから、結果先ほど実験していた内容はすべて、former_address のアドレスの指し示す先を読み出しているに過ぎないという話になる。former_address がまだそのメモリ領域に対する所有権を持ち続けているから、結果その領域が解放されずに残り続けているということになる。そしてこれなら double free が起きる理由も説明付きそう。

#[derive(Debug)]
struct SomeData {
    name: String
}

fn some_take_ownership(some: SomeData) {
    println!("took ownership: {:?}", &some as *const SomeData);
}

fn some_borrowing(some: &SomeData) {
    println!("borrowed: {:?}", some as *const SomeData);
}

fn main() {
    let data1 = SomeData { name: String::from("aiueo") };
    let data2 = SomeData { name: String::from("kakiku") };
    let data3 = SomeData { name: String::from("sasisu") };
    
    println!("data1 address: {:?}", &data1 as *const SomeData);
    println!("data2 address: {:?}", &data2 as *const SomeData);
    println!("data3 address: {:?}", &data3 as *const SomeData);
    
    some_take_ownership(data1);
    some_take_ownership(data2);
    some_take_ownership(data3);

    unsafe {
        println!("[data1] address: {:?}, value: {:?}", &data1 as *const SomeData, std::ptr::read(&data1 as *const SomeData));
        println!("[data2] address: {:?}, value: {:?}", &data2 as *const SomeData, std::ptr::read(&data2 as *const SomeData));
        println!("[data3] address: {:?}, value: {:?}", &data3 as *const SomeData, std::ptr::read(&data3 as *const SomeData));
    }
}
yukiyuki
#[derive(Debug)]
struct SomeData {
    name: String,
}

fn some_take_ownership(some: SomeData) -> *const SomeData {
    println!("took ownership: {:?}", &some as *const SomeData);
    &some as *const SomeData
}

fn some_take_address_ownership(address: *const SomeData) {
    println!("take_address_ownership: {:?}", address);
}

fn main() {
    let data = SomeData {
        name: String::from("hoge"),
    };
    let former_address = &data as *const SomeData;
    println!("former_address: {:?}", former_address);
    println!("data_address: {:?}", &data as *const SomeData);

    some_take_ownership(data);
    some_take_address_ownership(former_address);

    unsafe {
        println!(
            "address: {:?}, value: {:?}",
            former_address,
            std::ptr::read(former_address)
        );
    }
}
former_address: 0x7ffee22b3e98
data_address: 0x7ffee22b3e98
took ownership: 0x7ffee22b3f68
take_address_ownership: 0x7ffee22b3e98
address: 0x7ffee22b3e98, value: SomeData { name: "hoge" }
ownership-prac(76672,0x119e86dc0) malloc: *** error for object 0x7fa3f5405cc0: pointer being freed was not allocated
ownership-prac(76672,0x119e86dc0) malloc: *** set a breakpoint in malloc_error_break to debug

これだとあんま意味ないか。take_address_ownership の address 変数がどこに確保されているのかが知りたいかも。

yukiyuki

なるほど??下記はアセンブラ。

__ZN14ownership_prac27some_take_address_ownership17h84d89b762f042337E:
Lfunc_begin115:
	.loc	22 11 0
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register %rbp
	subq	$112, %rsp
	movq	%rdi, -88(%rbp)
Ltmp424:
	.loc	22 12 5 prologue_end
	leaq	-88(%rbp), %rax
	movq	%rax, -16(%rbp)
	movq	-16(%rbp), %rdi
	movq	%rdi, -8(%rbp)
Ltmp425:
	.loc	22 12 5 is_stmt 0
	leaq	__ZN52_$LT$$BP$const$u20$T$u20$as$u20$core..fmt..Debug$GT$3fmt17hc2030a3b483b433fE(%rip), %rsi
	callq	__ZN4core3fmt10ArgumentV13new17h245ce1b3b1032608E
	movq	%rax, -104(%rbp)
	movq	%rdx, -96(%rbp)
	.loc	22 0 5
	movq	-96(%rbp), %rax
	movq	-104(%rbp), %rcx
	.loc	22 12 5
	movq	%rcx, -32(%rbp)
	movq	%rax, -24(%rbp)
Ltmp426:
	.loc	22 12 5
	leaq	-32(%rbp), %rcx
	leaq	-80(%rbp), %rdi
	leaq	l___unnamed_8(%rip), %rsi
	movl	$2, %edx
	movl	$1, %r8d
	callq	__ZN4core3fmt9Arguments6new_v117h6148f90c53762d7eE
	leaq	-80(%rbp), %rdi
	callq	__ZN3std2io5stdio6_print17h11fd2e9918dd30f6E
	.loc	22 13 2 is_stmt 1
	addq	$112, %rsp
	popq	%rbp
	retq
Ltmp427:
Lfunc_end115:
	.cfi_endproc

	.p2align	4, 0x90

下記はデバッグして各レジスタに何が入っているかを確認してみたもの。

このスクラップは2021/09/19にクローズされました