我在下面實作了一個帶有可變引數的函式:
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#define ERR_BUFFER_SIZE 4096
void errPrint(const char *format, ...)
{
va_list argptr;
va_start(argptr, format);
char buf[ERR_BUFFER_SIZE];
vsnprintf(buf, sizeof(buf), format, argptr);
fprintf(stderr, "%s", buf);
va_end(argptr);
}
然后我希望基于上面的函式實作另一個名為“errExit()”的函式。我只是嘗試如下。它按我希望的那樣作業,但我認為它不正確。
void errExit(const char *format, ...)
{
errPrint(format);
exit(EXIT_FAILURE);
}
我試過errExit("hello,%s,%s,%s", "arg1","arg2", "arg3");
了,它正確列印了“ hello,arg1,arg2,arg3
”。
但是在我添加如下兩行代碼后,它拋出了錯誤Segmentation fault
。
void errExit(const char *format, ...)
{
char buf[4096];//added
strcpy(buf, format);//added
errPrint(format);
exit(EXIT_FAILURE);
}
我很困擾。據我了解:
format
被呼叫函式的堆疊中只有一個引數errPrint()
。我不敢相信va_start()
會從它的 parent-function 獲得論據errExit()
。- 既然
errPrint()
作業“正確”,為什么我添加兩行代碼后它不起作用?添加的代碼似乎沒有效果。
(我的英文不好,希望大家能接受我的說法。謝謝!)
uj5u.com熱心網友回復:
在errExit()
您需要通過引數將變數引數傳遞給另一個函式va_list
。因為你不是,vsnprintf
最終會在嘗試訪問堆疊上不存在的引數時崩潰。
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ERR_BUFFER_SIZE 4096
void verrPrint(const char *format, va_list ap) {
char buf[ERR_BUFFER_SIZE];
vsnprintf(buf, sizeof(buf), format, ap);
fprintf(stderr, "%s", buf);
}
void errPrint(const char *format, ...) {
va_list ap;
va_start(ap, format);
verrPrint(format, ap);
va_end(ap);
}
void errExit(const char *format, ...) {
va_list ap;
va_start(ap, format);
verrPrint(format, ap);
va_end(ap);
exit(EXIT_FAILURE);
}
int main(void) {
errPrint("What is the meaning of %s\n", "life?");
errExit("%d\n", 42);
return 0;
}
uj5u.com熱心網友回復:
其他答案解釋了您需要做什么。這個答案是關于它為什么會起作用的 asm 細節。
在您的第一個errExit
中,如果您查看 x86-64 的編譯器生成的程式集,所有傳入的引數在呼叫時仍將在暫存器中errPrint(format);
。即使您沒有告訴編譯器傳遞它們,它也不會在呼叫其他函式之前執行任何使用暫存器的操作。
因此,即使您沒有明確傳遞第一個 ( )之外的任何引數,它也能正常作業。format
它的編譯就像您撰寫了一個傳遞其前 6 個引數的函式,假設 x86-64 System V 呼叫約定,并且引數都是整數或指標(例如char*
)。
在 C 抽象機器中,您的代碼在生成errPrint
ava_list argptr
并將其傳遞給vsnprintf
參考該串列中超過 0 個元素的函式 ( ) 時是沒有意義的并且具有未定義的行為。
查看 asm 中傳遞多個引數的函式可能會有所幫助:
void foo(char *a, char *b, char *c, char *d, char *f);
int bar(char *a, char *b, char *c, char *d, char *f)
{
foo(a, b, c, d, f);
return 1; // some code after the call, so it can't compile to a tailcall
}
在 Godbolt 上,使用 GCC12 為 Linux 編譯:
# GCC12 -O3
bar:
# incoming args are in RDI, RSI, RDX, RCX, R8, R9 in that order
# same place a callee will look for them, so passing them on is trivial
sub rsp, 8 # re-align the stack so RSP == 0
call foo
mov eax, 1 # return-value register = 1
add rsp, 8 # restore the stack pointer
ret # pop return address into RIP
你errExit
在呼叫之前有等效的 asm,所以如果有暫存器引數,即使你沒有告訴 C 編譯器,它們也會被傳遞。
errExit:
sub rsp, 8
xor eax, eax # Variadic functions get AL = # of args passed in XMM regs
call errPrint
mov edi, 1
call exit # GCC knows exit() is noreturn
如果之前有任何代碼call errPrint
,例如對 的呼叫strcpy
,那當然會踩到那些傳遞引數的暫存器。 所以它們在call errPrint
運行時有不同的值,除了你告訴 C 編譯器的一個引數。它將保存在跨過一個呼叫保留的暫存器中call strcpy
。
如果您將其編譯為具有較少暫存器引數的呼叫約定,例如gcc -m32
只有堆疊引數的 32 位 x86 ( ),則使用轉換vsnprintf
參考的指標來自的堆疊幀,高于它打算傳遞的任何引數。所以可能是對齊的填充堆疊槽,然后在除錯構建中可能是保存的 EBP,甚至可能是自己的回傳地址。%s
errExit
errExit
如果您很好奇,請使用%p
轉換來列印指標值,而不是嘗試取消參考和段錯誤。
C 沒有辦法指定它應該只傳遞暫存器引數;如果您想從 a 傳遞未知數量的 args ...
,則必須使用va_list
適用于任意數量 args 的 a 來完成,包括 x86-64 System V 上的 7 個或更多。這是一件好事,因為有些目標不會有任何暫存器引數,所以只需保存/恢復暫存器引數就會傳遞 0 個引數。
我展示 asm 只是為了理解為什么一個版本碰巧可以作業,而不是建議你可以做任何事情來撰寫可以像這樣編譯為 asm 的安全且可移植的 C,即使在call errPrint
.
uj5u.com熱心網友回復:
謝謝大家。我從你的回答中學到了你的知識。我選擇如下更改我的代碼。
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ERR_BUFFER_SIZE 4096
void errPrint(const char *format, va_list arg_list)
{
char buf[ERR_BUFFER_SIZE];
vsnprintf(buf, sizeof(buf), format, arg_list);
fprintf(stderr, "%s", buf);
}
void errExit(const char *format, va_list arg_list)
{
errPrint(format, arg_list);
exit(EXIT_FAILURE);
}
void v_exec(void (*func)(const char *, va_list), const char *format, ...)
{
va_list arg_list;
va_start(arg_list, format);
func(format, arg_list);
va_end(arg_list);
}
int main(void)
{
v_exec(errExit,"%s%d", "hello",520);
return 0;
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/505315.html