#include <stdint.h>
#include <stdarg.h>
#include <stddef.h>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
HANDLE inHandle = INVALID_HANDLE_VALUE;
HANDLE outHandle = INVALID_HANDLE_VALUE;
#else
#include <unistd.h>
#endif
int stdoutFunction(const char* buffer, size_t bufferSize) {
#ifdef _WIN32
int bytesWritten = 0;
WriteConsoleA(outHandle, buffer, bufferSize, &bytesWritten, NULL);
return bytesWritten;
#else
return write(1, buffer, bufferSize);
#endif
}
size_t copyAscii(char* buffer, const char* src, size_t charSize, size_t bufferIdx, const size_t maxBufferIdx) {
for (; *src != '0円'; src += charSize) {
buffer[bufferIdx] = *src;
bufferIdx += (bufferIdx < maxBufferIdx);
}
return bufferIdx;
}
size_t intToHexStr(char* buffer, const size_t n, size_t bufferIdx, const size_t maxBufferIdx) {
const char hexCharacters[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
for (int32_t shiftRightBy = (sizeof(size_t) * 8) - 4; shiftRightBy >= 0; shiftRightBy -= 4) {
buffer[bufferIdx] = hexCharacters[(n >> shiftRightBy) % 16];
bufferIdx += (bufferIdx < maxBufferIdx);
}
return bufferIdx;
}
size_t intToStr(char* buffer, size_t n, size_t bufferIdx, const size_t maxBufferIdx) {
size_t frontIdx = bufferIdx;
do {
buffer[bufferIdx] = '0' + (n % 10);
bufferIdx += (bufferIdx < maxBufferIdx);
n /= 10;
} while (n > 0);
size_t backIdx = bufferIdx - 1;
while (frontIdx < backIdx) {
char tmp = buffer[frontIdx];
buffer[frontIdx] = buffer[backIdx];
buffer[backIdx] = tmp;
frontIdx += 1;
backIdx -= 1;
}
return bufferIdx;
}
// working format specifiers: s, ls, d, u, zu, x, %
int dumberPrintf(const char* fmt, ...) {
if (fmt[0] == '0円') {
return 0;
}
char buffer[512];
size_t bufferIdx = 0;
size_t maxBufferIdx = sizeof(buffer) - 1;
va_list args;
va_start(args, fmt);
for (size_t fmtIdx = 0; fmt[fmtIdx] != '0円'; fmtIdx++) {
if (fmt[fmtIdx] == '%') {
if (
fmt[fmtIdx + 1] == 's'
|| (fmt[fmtIdx + 1] == 'l' && fmt[fmtIdx + 2] == 's')
) {
bufferIdx = copyAscii(
buffer,
fmt[fmtIdx + 1] == 's' ? va_arg(args, char*) : (char*)va_arg(args, wchar_t*),
fmt[fmtIdx + 1] == 's' ? sizeof(char) : sizeof(wchar_t),
bufferIdx,
maxBufferIdx
);
fmtIdx += (fmt[fmtIdx + 1] == 'l');
}
else if (
fmt[fmtIdx + 1] == 'd'
|| fmt[fmtIdx + 1] == 'u'
|| (fmt[fmtIdx + 1] == 'z' && fmt[fmtIdx + 2] == 'u')
) {
bufferIdx = intToStr(buffer, va_arg(args, size_t), bufferIdx, maxBufferIdx);
fmtIdx += (fmt[fmtIdx + 1] == 'z');
}
else if (fmt[fmtIdx + 1] == 'x') {
bufferIdx = intToHexStr(buffer, va_arg(args, size_t), bufferIdx, maxBufferIdx);
}
else {
buffer[bufferIdx] = '%';
bufferIdx += (bufferIdx < maxBufferIdx);
}
fmtIdx += 1;
}
else {
buffer[bufferIdx] = fmt[fmtIdx];
bufferIdx += (bufferIdx < maxBufferIdx);
}
}
va_end(args);
return stdoutFunction(buffer, bufferIdx);
}
int main() {
#ifdef _WIN32
inHandle = GetStdHandle(STD_INPUT_HANDLE);
outHandle = GetStdHandle(STD_OUTPUT_HANDLE);
if (inHandle == INVALID_HANDLE_VALUE || outHandle == INVALID_HANDLE_VALUE) {
return GetLastError();
}
#endif
dumberPrintf("%%s test: %s\n%%ls test: %ls\n%%d test: %d\n%%u test: %u\n%%zu test: %zu\n%%x test: %x\n",
"char string",
L"wchar_t string",
12345,
54321,
56789,
0xabcdef
);
return 0;
}
/*
int __stdcall mainCRTStartup() {
int result = main();
return result;
}
*/
I'm making something which I'm going to update to not use any C runtime functions, including printf. I'm going to compile with /NODEFAULTLIB
. I made this function to imitate printf. It works with the format specifiers s
, ls
, d
, u
, zu
, x
, and %
. It's not expected to print any negative numbers. It's also not expected to print anything longer than 512 bytes, so it cuts off anything beyond that length.
2 Answers 2
Use the standard library
There is no reason to use /NODEFAULTLIB
- Use
printf
instead of rolling your solution. But since you are probably removing the standard library. Here you go. write
does not promise that all bytes will be written. If the kernel buffers are smaller than the number of bytes to be written it will only write a few bytes. You have to address that in yourstdoutFunction
.- You might want to create two different definitions for Windows and Linux and then choose which one to use based on a macro, instead of using macros in the same function. It makes the code easier to read. But that is just my style. People may differ.
- When writing parsing code, don't loop over the parse string character by character. Instead, think in terms of tokens.
You can break your problem into tokenizing the format string and doing the correct stuff in a
switch
statement based on the token, like below. It is much more clear than mixingwchar_t
andchar
strings. It also leads to fewer bugs because you are focusing on printing a single in eachcase
statement.
typedef enum {
ConversionType_StringLiteral = 0,
ConversionType_String,
ConversionType_WideString,
//...
ConversionType_MAX
} ConversionType;
typedef struct ConversionSpecification {
ConversionType type;
size_t length;
size_t precision;
}ConversionSpecification;
ConversionSpecification getFormatSpecification(const char* fmt){
}
int dumberPrintf(const char* fmt, ...) {
va_list args;
va_start(args, fmt);
size_t bytesWritten = 0;
while(*fmt){
ConversionSpecification spec = getFormatSpecification(fmt);
if(spec.type >= ConversionType_MAX){
return -1;
}
switch(spec.type){
case ConversionType_StringLiteral:
{
// Do string literal stuff
}
case ConversionType_String:
{
// Do string stuff
}
case ConversionType_WideString:
{
// Do wide string stuff stuff
}
default:
}
fmt += spec.length;`
}
va_end(args);
return bytesWritten;
}
-
1\$\begingroup\$ Thank you. Also, I read your comment, but I thought you weren't supposed to use standard library functions in __attribute__((constructor)) because the standard library might need things to be initialized too. \$\endgroup\$my_stack_exchange_account– my_stack_exchange_account2023年12月14日 16:33:56 +00:00Commented Dec 14, 2023 at 16:33
-
1\$\begingroup\$ The C runtime is what calls
__attribute__((constructor))
and then main. The C runtime also contains all external linkages to the C standard library. It is true that neither POSIX nor the C standard guarantees that the C runtime will be initialized before__attribute__((constructor))
. But that is because none of them recognize the existence of__attribute__((constructor))
. It is a compiler-specific attribute. What is your use case though? There might be better ways to implement it instead of doing this \$\endgroup\$Ajinkya Kamat– Ajinkya Kamat2023年12月14日 18:40:22 +00:00Commented Dec 14, 2023 at 18:40 -
\$\begingroup\$ I'm writing to a game's executable segment to mod it for a speedrunning tool. On Linux, I'm doing this using LD_PRELOAD. I also can't just change the game's executable because the moderators said they wouldn't allow that. \$\endgroup\$my_stack_exchange_account– my_stack_exchange_account2023年12月14日 18:48:11 +00:00Commented Dec 14, 2023 at 18:48
-
\$\begingroup\$ this is how you get perma bans. You know that anti-cheat will easily catch you if you try to inject a dll right? They also scan and hash your segments and send them to a server for verification. Also, how is attribute constructor going to help you with changing another program's executable segment? \$\endgroup\$Ajinkya Kamat– Ajinkya Kamat2023年12月14日 19:29:18 +00:00Commented Dec 14, 2023 at 19:29
-
\$\begingroup\$ There is no anti-cheat system, it's a speedrunning-related mod for a single player game. I use LD_PRELOAD to load a .so file which uses __attribute__((constructor)) to set things up and write to the game's executable segment when it starts up. \$\endgroup\$my_stack_exchange_account– my_stack_exchange_account2023年12月14日 19:52:36 +00:00Commented Dec 14, 2023 at 19:52
A few points to add:
I have no idea why you will or won't mark any parameter you could
const
. At best, you are awfully inconsistent in that area.
You don't get the benefits of making what you can constant, small as they are when the scope is small. Nor do you get the benefit of not cluttering the prototype with inconsequential noise.I suspect at least some of the functions should be purely for internal use. Consider marking them
static
to enforce that, and keep them out of any exports.Even though it might be made static by the compiler under the as-if rule due to how it is used, your
const
local has auto storage duration, which might result in extra overhead.var += 1
andvar -= 1
are more verbose and much less common thanvar++
andvar--
.
printf
then just... Use that and don't passNODEFAULTLIB
. \$\endgroup\$