96937


nasmとgccを用いてアセンブラを勉強中です  
動作環境はWindows11です

Cとアセンブラを連携するため、以下のコードを書きました

[code]

    ;Asm.asm
    
    bits 64
    
    extern print
    
    section .data
    message: db "Success", 10
    
    section .text
    global Test
    
    Test:
        mov rcx,message
        call print
        ret
    
[/code]

[code]

    //Win.c
    
    #include <stdio.h>
    
    extern char* Test();
    
    int main()
    {
            printf("%s",Test());
    }
    
    char* print(char* c)
    {
            printf("%s",c);
            return c;
    };
    
[/code]

`Win.c`の`print関数`を`Asm.asm`で呼び出し、その引数に渡した文字列`Success`を戻り値として`Asm.asm`側の`Test関数`で取得後、Cの`main関数`へ渡します  
`print関数`と`main関数`ではそれぞれ`printf関数`を宣言しています

以上のプログラムから、以下のコマンドを使用して、実行ファイル`a.exe`を生成します

[code]

    nasm -f elf64 Asm.asm
    gcc Win.c -c
    gcc Win.o Asm.o
    
[/code]

`a.exe`を起動すると、以下の結果が返ります

[code]

    Success
    
    
[/code]

`printf関数`はコード中で２度宣言しているので、もしもファイルが正しく実行される場合は、以下の結果が期待されます

[code]

    Success
    Success
    
[/code]

しかし、`Success`は一つしか表示されず、直下が空白行となります  
`print関数`の引数に与えた文字列は、そのスコープ内の`printf関数`に正しく渡っているようですが、その後の処理のどこかの段階でファイルが強制終了されている様子です

戻り値はx64の場合`raxレジスタ`に保存されるという解説を見たので、`print関数`からの戻り値は直接そのレジスタに渡っていると考え、直後にretを置きました

しかし、結果だけを見ると`Test関数`の戻り値は`main関数`へ返されていないように見えます  
どうすれば`print関数`からの戻り値`Success`を正しく`main関数`へ渡せるでしょうか？


CC BY-SA 4.0
2023-11-29T07:17:45.460
96932
2023-12-02T09:26:24.220
60567
question
c gcc assembly
アセンブラで呼び出したCの関数から戻り値を取得する方法


C言語における文字列、というのはどういうものか、を確認しましょう  
本来なら、'\0'を終端に付けた文字配列、のはずですが、あなたのコードではそうなっていません。  
また、あなたの使っているコンパイラ（GCC)の呼出規約を確認しましょう。  
そのとおりになっていますか？


CC BY-SA 4.0
2023-11-29T08:40:36.893
96934
2023-11-29T08:40:36.893
27481
96932
answer


`amd x64 abi` っぽく見えますがそうじゃないかもしれませんので以下は憶測なのですが

`amd x64 abi` のごく一部を解説した記事
[アセンブリ言語で書かれた関数をC言語に書き直す](https://ja.stackoverflow.com/questions/38341/)
では触れなかったのですが `amd x64 abi` では `rsp` レジスタを 16byte align
する義務があります。今ここで真面目に解説すると長くなるので外部記事へリンク  
[x64 でのスタックの使用](https://learn.microsoft.com/ja-jp/cpp/build/stack-
usage?view=msvc-170)  
[x86-64
モードのプログラミングではスタックのアライメントに気を付けよう](https://uchan.hateblo.jp/entry/2018/02/16/232029)

提示コードは  
\- C コンパイラが生成したコードが `call Test` する直前の `rsp` は 16 の倍数です。  
\- `call Test` 命令によって `rsp` は 8 減算されます（整合されなくなりました）  
\- その状態であなたのコードが `call print` をしてしまうと `rsp` が整合されていないので abi 違反  
\- つまり暴走しても強制終了しても文句言えない  
\- 正しく動かすには `rsp` の再整合を行う prologue epilogue コードが必要

もし正しく `print()` 関数が呼び出せてかつ戻ってこれるなら、その返却値が `rax`
に格納される、というのは認識の通りです。が、現状不正な手続きで `print()` 関数を呼んでいるため `print()` 処理中にエラーが発生し OS
によって強制終了させられています。なので結果的には「取得できない」ように見えています。 abi 仕様を満足させるべく `rsp`
のアラインメントを正しく取るように修正するとうまく動くと思われます。

# 既に指摘のある文字列の終端 `\0` に関しては正しく修正が必要

x86 や x64 の abi は、アセンブラコードを人間が手書きするには難しすぎてお勧めできないです。この例においても当該 `Test()` を C
ソースで書いてアセンブラ出力させると予想しないほど面倒な prologue epilogue コードが生成されているのが観測されるはずです。

`mov rcx,message` があなたの期待した動作をするかについては触れないでおくっス。多分こっちも修正する必要がありそう（ `nasm`
は全然使っていないので保留）

オイラの後輩君がアセンブラ始めたいって言うなら Arm Cortex とかをお勧めします。 Raspberry pi が流行してるのは伊達ではないっス。


CC BY-SA 4.0
2023-11-30T00:19:24.703
96937
2023-11-30T04:20:42.543
2023-11-30T04:20:42.543
8589
8589
96932
answer


返信欄で文章が上手く改行されないので、回答欄より失礼します  
**774RRさん** の回答を受け、`16byte align`というワードをヒントにコードを修正しました

まだ理解は追いついていないのですが、まずこの **１６の倍数に整合させる** というのは **関数コールの直前にとりあえずrspを追加で８減算しておく**
ということでしょうか？  
関数コールをすると **リターンアドレスの領域が確保される**
と思いますので、その影響で`rsp`の減算によりスタック領域が確保され、１６の倍数から外れる、という解釈で合っていますか？

以上のように考え、`Test関数`を以下のように整形しました

[code]

    ;Asm.asm
    
    bits 64
    
    extern print
    
    section .data
    message: db "Success",10
    
    section .text
    global Test
    
    Test:
        sub rsp,8
        mov rcx,message
        call print
        add rsp,8
        ret
    
[/code]

関数の終了時には`ret`によって直前のアドレスに格納された整数がリターンアドレスと認識されるはずなので、`main関数`からの`Test関数`のコール時に
**スタックへプッシュされた値が格納されるアドレス** を指し直すように関数コール後にrspの値を元に戻しています

以上のコードより再度生成した`a.exe`を実行すると、以下の結果が得られました

[code]

    Success
    Success
    
[/code]

今度は、期待された通りに動作しています

一応、実現したいことの本懐は達成されたので質問自体は解決なのですが、上記のコードが安全なのか、引数の記述を修正した方がいいという指摘を受けたことも含め、不安が残っています  
セオリーとなる記述形式があれば知りたいです

なにはともあれ、ヒントを提示頂いた **774RRさん** をはじめ、回答を下さった方々に感謝します  
ありがとうございました


CC BY-SA 4.0
2023-12-01T18:06:41.467
96954
2023-12-01T19:13:08.060
2023-12-01T19:13:08.060
60567
60567
96932
answer


# 回答ではありません

mingw64のgcc(GNU cc)とGNU asを使って動く参考コード(bashスクリプト)です。

[code]

    cat >win.c <<EOF
    //win.c
    
    #include <stdio.h>
    
    extern char* Test();
    
    int main()
    {
            printf("%s",Test());
    }
    
    char* print(char* c)
    {
            printf("%s",c);
            return c;
    }
    
    char* print_address(void* p)
    {
            printf("%p\n", p);
            return p;
    }
    EOF
    cat >asm.S <<EOF
    #asm.S
    
    .section .text
    message: .asciz "Success\\n"
    
    .global Test
    
    Test:
        # shadow spaceを0クリアしておく
        push    %rbp
        xor     %rcx, %rcx
        push    %rcx
        push    %rcx
        push    %rcx
        push    %rcx
        mov     %rsp, %rbp
    
        # 元の呼出
        lea     message(%rip), %rcx
        call    print
    
        # shadow spaceの内容を出力
        sub     \$0x20, %rsp    # 出力用にもう一段shadow spaceを用意
        mov     0(%rbp), %rcx
        call    print_address
        mov     8(%rbp), %rcx
        call    print_address
        mov     16(%rbp), %rcx
        call    print_address
        mov     24(%rbp), %rcx
        call    print_address
        add     \$0x20, %rsp    # 追加したshadow spaceを削除
        
        # shadow spaceを削除
        add     \$0x20, %rsp
        pop     %rbp
        lea     message(%rip), %rax
        ret
    EOF
    gcc -g -c win.c
    gcc -g -c asm.S
    gcc win.o asm.o -o hoge
    ./hoge
    # 出力例
    # Success
    # 00007ff704c11530
    # 0000000000000000
    # 0000000000000000
    # 0000000000000000
    # Success
    #
    # print呼出時にshadow spaceの先頭8バイト(第一引数)が書き換えられてるのが分かる
    
[/code]

あと参考リンク  
<https://en.wikipedia.org/wiki/X86_calling_conventions#Microsoft_x64_calling_convention>


CC BY-SA 4.0
2023-12-02T09:26:24.220
96957
2023-12-02T09:26:24.220
54957
96932
answer