As linguagens de programação C e C++ possuem uma relação. Praticamente vivem em harmonia e sintonia – mesmo existindo compiladores e padrões distintos. Essa combinação é que possibilita escrevermos abstrações “próximo ao metal”, estabelecendo um equilíbrio entre a compreensão (do código, que depende de ponto de vista e conhecimento) e o desempenho.
O problema é quando truques e hacks entram em ação. Um dos meus favoritos com C++ é sobrescrever no buffer de uma string. Isso é feito através do retorno do método c_str(), que disponibiliza um ponteiro para este buffer interno. No entanto, é necessário fazer um const_cast para remover o constness do ponteiro e assim sobrescreve-lo.
Os exemplos que elaborei para este post são uma tentativa de simular um problema real que me custou quase um dia inteiro de depuração, por causa de um hacking “metido a besta”. :-)
Sabemos que a maioria dos sistemas operacionais, por exemplo, Windows ou Linux, fornecem APIs em C para uma parte significativa de seus subsistemas. Até mesmo algumas funções da biblioteca padrão do C são necessárias por não existir no C++ ou simplesmente por estarmos familiarizados de longa data com elas. E acaba sendo natural utilizá-las (usar diretamente) ou encapsulá-las (usar indiretamente).
Agora, onde uma string (por exemplo, std::string ou std::wstring) entra nisso tudo? Ok, vamos lá.
Algumas APIs ou funções C lidam com ponteiros de caracteres (char* ou wchar_t*), e é neste ponteiro que devemos informar um buffer. Por exemplo, API do Windows GetUserName, sobrescreve um array de caracteres com o nome do usuário que está no contexto da execução:
[code language="cpp"]
wchar_t buffer[64];
DWORD size = sizeof(buffer) / sizeof(buffer[0]);
if (GetUserName(buffer, &size))
return buffer;
[/code]
Note que ela recebe no segundo argumento atual o endereço de uma variável contendo o tamanho de caracteres do buffer (fique atento pois algumas funções podem receber o tamanho em bytes), ela será sobrescrita com a quantidade de caracteres escrito no buffer.
Estando no C++, imagine encapsular está funcionalidade numa função chamada username. Uma assinatura natural para esta função é:
[code language="cpp"]
std::wstring username()
[/code]
Onde, ela retorna o conteúdo numa std::wstring. E a implementação completa pode ser:
[code language="cpp"]
std::wstring username()
{
wchar_t buffer[64];
DWORD size = sizeof(buffer) / sizeof(buffer[0]);
if (GetUserName(buffer, &size))
return buffer; //converted implicitly to std::wstring
throw std::runtime_error("GetUserName failed");
}
[/code]
Podendo ser consumida da seguinte forma:
[code language="cpp"]
const std::wstring& s = username();
std::wcout << s << "\n";
std::cout << hex_rep(buffer_to_transmit(s)) << "\n";
[/code]
Produzindo um possível resultado do meu usuário e uma representação do buffer em hexadecimal, que supostamente seria transmitido através de algum meio de comunicação:
fgaluppo
Size: 16 bytes
6600670061006C007500700070006F00
Ao olharmos a função username, notaremos que o buffer retornado é uma cópia convertida implicitamente para uma std::wstring. O ideal seria evitar a cópia temporária e retorná-la, aproveitando os benefícios do move constructor. Ai é onde o nosso hacking entrará. Ao invés de criar um array de wchar_t, porque não alocar uma std::wstring e passar seu buffer para ser sobrescrito? Abaixo um ajuste do código para suportar esta idéia:
[code language="cpp"]
std::wstring username2()
{
std::wstring buffer;
buffer.resize(64);
DWORD size = buffer.size();
wchar_t* buffer_ptr = const_cast<wchar_t*>(buffer.c_str());
if (GetUserName(buffer_ptr, &size))
return buffer;
throw std::runtime_error("GetUserName failed");
}
[/code]
Ou seja, é passado para a função GetUserName um ponteiro válido com conteúdo mutável, obtido através de:
[code language="cpp"]
wchar_t* buffer_ptr = const_cast<wchar_t*>(buffer.c_str());
[/code]
Se imprimirmos no console (ou redirecionarmos a stream de saída) com std::wcout com o retorno da função username2, o resultado será o mesmo:
fgaluppo
E tudo parece ok. O parecer correto se dá por causa que ao final, o conteúdo do buffer, termina com ‘\0’ - wchar_t com 2 bytes adjacentes ‘\0’ e ‘\0’. Isso é o suficiente para o std::wcout imprimir uma string corretamente. No entanto, o tamanho da string mantido pelo std::wstring (ou std::string) pode ser diferente, no caso do exemplo, é 64 – conforme informado em resize. Este tamanho não foi sincronizado com o buffer sobrescrito. É ai que um bug fica exposto, veja a representação do hexadecimal do buffer a ser transmitido:
Size: 128 bytes
6600670061006C007500700070006F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Exceto pelos bytes iniciais, ele é diferente da versão original que tem apenas 16 bytes para serem transmitidos!
O problema foi exposto pela função buffer_to_transmit, que considera a string e o seu membro do tipo função size – o que faz sentido afinal é um membro que faz parte do objeto, e ele devia estar consistente:
[code language="cpp"]
std::vector<char> buffer_to_transmit(const std::wstring& s)
{
std::vector<char> temp;
size_t N = 2 * s.size();
temp.resize(N);
const char* ptr = reinterpret_cast<const char*>(s.c_str());
std::copy(ptr, ptr + N, temp.begin());
return std::move(temp);
}
[/code]
O que aconteceu aqui foi:
Logo, o conteúdo do buffer interno, não estava refletindo corretamente com o size, ou vice-versa. Aliás, isto pode ser visto como uma feature (um comportamento esperado) ou como um bug. No entanto, corrigir isto me parece trivial, pois a API GetUserName retorna a quantidade de caracteres sobrescritos. Então, uma implementação correta é:
[code language="cpp"]
std::wstring username3()
{
std::wstring buffer;
buffer.resize(64);
DWORD size = buffer.size();
wchar_t* buffer_ptr = const_cast<wchar_t*>(buffer.c_str());
if (GetUserName(buffer_ptr, &size))
{
--size; //The second argument of GetUserName computes the '\0'
buffer.resize(size); //The (w)string resize performs the actual size + 1
return buffer;
}
throw std::runtime_error(&GetUserName failed&);
}
[/code]
Ou seja, efetuar o resize novamente com a quantidade de caracteres sobrescritos. Agora pareceu fácil e sem problemas! :-)
Existem APIs que não retornam a quantidade de caracteres ou bytes escritos no buffer. Por exemplo, a API do C, para formatação de data e horário, strftime:
[code language="cpp"]
tm* t = localtime(&now);
strftime(buffer, buffer.size(), &%Y-%m-%d %T&, t);
[/code]
Neste caso, para encurtarmos caminho, é só executar a função strlen com o buffer interno da string (ou c_str – afinal isto quer dizer C string) dessincronizada. Uma implementação correta encapsulando esta função é:
[code language="cpp"]
std::string timestamp3()
{
time_t now = time(nullptr);
tm* t = localtime(&now);
if (nullptr == t)
throw std::runtime_error(&localtime failed&);
std::string buffer(64, ' ');
char* buffer_ptr = const_cast<char*>(buffer.c_str());
if (0 == strftime(buffer_ptr, buffer.size(), &%Y-%m-%d %T&, t))
throw std::runtime_error(&strftime failed&);
std::string temp(buffer_ptr, std::strlen(buffer_ptr)); //converted explicitly to std::string using ptr and len
return temp;
}
[/code]
Se executarmos as três versões desta forma:
[code language="cpp"]
for (const std::string& s : { timestamp(), timestamp2(), timestamp3() })
{
std::cout << s << &\n&;
std::cout << hex_rep(buffer_to_transmit(s)) << &\n&;
}
[/code]
Obteremos uma saída similar há:
2016-05-10 19:16:31
Size: 19 bytes
323031362D30352D31302031393A31363A3331
2016-05-10 19:16:31
Size: 64 bytes
323031362D30352D31302031393A31363A3331002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020
2016-05-10 19:16:31
Size: 19 bytes
323031362D30352D31302031393A31363A3331
Onde as versões 1 e 3 serão os resultados esperados.
Abaixo uma compilação e execução do exemplo timestamp:

Fontes:
https://github.com/SimplyCpp/examples/tree/master/c_cpp_string
O problema é quando truques e hacks entram em ação. Um dos meus favoritos com C++ é sobrescrever no buffer de uma string. Isso é feito através do retorno do método c_str(), que disponibiliza um ponteiro para este buffer interno. No entanto, é necessário fazer um const_cast para remover o constness do ponteiro e assim sobrescreve-lo.
Os exemplos que elaborei para este post são uma tentativa de simular um problema real que me custou quase um dia inteiro de depuração, por causa de um hacking “metido a besta”. :-)
Sabemos que a maioria dos sistemas operacionais, por exemplo, Windows ou Linux, fornecem APIs em C para uma parte significativa de seus subsistemas. Até mesmo algumas funções da biblioteca padrão do C são necessárias por não existir no C++ ou simplesmente por estarmos familiarizados de longa data com elas. E acaba sendo natural utilizá-las (usar diretamente) ou encapsulá-las (usar indiretamente).
Agora, onde uma string (por exemplo, std::string ou std::wstring) entra nisso tudo? Ok, vamos lá.
Algumas APIs ou funções C lidam com ponteiros de caracteres (char* ou wchar_t*), e é neste ponteiro que devemos informar um buffer. Por exemplo, API do Windows GetUserName, sobrescreve um array de caracteres com o nome do usuário que está no contexto da execução:
[code language="cpp"]
wchar_t buffer[64];
DWORD size = sizeof(buffer) / sizeof(buffer[0]);
if (GetUserName(buffer, &size))
return buffer;
[/code]
Note que ela recebe no segundo argumento atual o endereço de uma variável contendo o tamanho de caracteres do buffer (fique atento pois algumas funções podem receber o tamanho em bytes), ela será sobrescrita com a quantidade de caracteres escrito no buffer.
Estando no C++, imagine encapsular está funcionalidade numa função chamada username. Uma assinatura natural para esta função é:
[code language="cpp"]
std::wstring username()
[/code]
Onde, ela retorna o conteúdo numa std::wstring. E a implementação completa pode ser:
[code language="cpp"]
std::wstring username()
{
wchar_t buffer[64];
DWORD size = sizeof(buffer) / sizeof(buffer[0]);
if (GetUserName(buffer, &size))
return buffer; //converted implicitly to std::wstring
throw std::runtime_error("GetUserName failed");
}
[/code]
Podendo ser consumida da seguinte forma:
[code language="cpp"]
const std::wstring& s = username();
std::wcout << s << "\n";
std::cout << hex_rep(buffer_to_transmit(s)) << "\n";
[/code]
Produzindo um possível resultado do meu usuário e uma representação do buffer em hexadecimal, que supostamente seria transmitido através de algum meio de comunicação:
fgaluppo
Size: 16 bytes
6600670061006C007500700070006F00
Ao olharmos a função username, notaremos que o buffer retornado é uma cópia convertida implicitamente para uma std::wstring. O ideal seria evitar a cópia temporária e retorná-la, aproveitando os benefícios do move constructor. Ai é onde o nosso hacking entrará. Ao invés de criar um array de wchar_t, porque não alocar uma std::wstring e passar seu buffer para ser sobrescrito? Abaixo um ajuste do código para suportar esta idéia:
[code language="cpp"]
std::wstring username2()
{
std::wstring buffer;
buffer.resize(64);
DWORD size = buffer.size();
wchar_t* buffer_ptr = const_cast<wchar_t*>(buffer.c_str());
if (GetUserName(buffer_ptr, &size))
return buffer;
throw std::runtime_error("GetUserName failed");
}
[/code]
Ou seja, é passado para a função GetUserName um ponteiro válido com conteúdo mutável, obtido através de:
[code language="cpp"]
wchar_t* buffer_ptr = const_cast<wchar_t*>(buffer.c_str());
[/code]
Se imprimirmos no console (ou redirecionarmos a stream de saída) com std::wcout com o retorno da função username2, o resultado será o mesmo:
fgaluppo
E tudo parece ok. O parecer correto se dá por causa que ao final, o conteúdo do buffer, termina com ‘\0’ - wchar_t com 2 bytes adjacentes ‘\0’ e ‘\0’. Isso é o suficiente para o std::wcout imprimir uma string corretamente. No entanto, o tamanho da string mantido pelo std::wstring (ou std::string) pode ser diferente, no caso do exemplo, é 64 – conforme informado em resize. Este tamanho não foi sincronizado com o buffer sobrescrito. É ai que um bug fica exposto, veja a representação do hexadecimal do buffer a ser transmitido:
Size: 128 bytes
6600670061006C007500700070006F0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Exceto pelos bytes iniciais, ele é diferente da versão original que tem apenas 16 bytes para serem transmitidos!
O problema foi exposto pela função buffer_to_transmit, que considera a string e o seu membro do tipo função size – o que faz sentido afinal é um membro que faz parte do objeto, e ele devia estar consistente:
[code language="cpp"]
std::vector<char> buffer_to_transmit(const std::wstring& s)
{
std::vector<char> temp;
size_t N = 2 * s.size();
temp.resize(N);
const char* ptr = reinterpret_cast<const char*>(s.c_str());
std::copy(ptr, ptr + N, temp.begin());
return std::move(temp);
}
[/code]
O que aconteceu aqui foi:
- A função username2 retornou uma std::wstring que previamente tinha o tamanho de 64 caracteres (ou 128 bytes, pois o sizeof de wchar_t é 2);
- Ocorreu um const_cast para sobrescrever o buffer interno desta string;
- O conteúdo retornado era menor do que o alocado previamente;
- Não houve uma sincronização do novo tamanho.
Logo, o conteúdo do buffer interno, não estava refletindo corretamente com o size, ou vice-versa. Aliás, isto pode ser visto como uma feature (um comportamento esperado) ou como um bug. No entanto, corrigir isto me parece trivial, pois a API GetUserName retorna a quantidade de caracteres sobrescritos. Então, uma implementação correta é:
[code language="cpp"]
std::wstring username3()
{
std::wstring buffer;
buffer.resize(64);
DWORD size = buffer.size();
wchar_t* buffer_ptr = const_cast<wchar_t*>(buffer.c_str());
if (GetUserName(buffer_ptr, &size))
{
--size; //The second argument of GetUserName computes the '\0'
buffer.resize(size); //The (w)string resize performs the actual size + 1
return buffer;
}
throw std::runtime_error(&GetUserName failed&);
}
[/code]
Ou seja, efetuar o resize novamente com a quantidade de caracteres sobrescritos. Agora pareceu fácil e sem problemas! :-)
Existem APIs que não retornam a quantidade de caracteres ou bytes escritos no buffer. Por exemplo, a API do C, para formatação de data e horário, strftime:
[code language="cpp"]
tm* t = localtime(&now);
strftime(buffer, buffer.size(), &%Y-%m-%d %T&, t);
[/code]
Neste caso, para encurtarmos caminho, é só executar a função strlen com o buffer interno da string (ou c_str – afinal isto quer dizer C string) dessincronizada. Uma implementação correta encapsulando esta função é:
[code language="cpp"]
std::string timestamp3()
{
time_t now = time(nullptr);
tm* t = localtime(&now);
if (nullptr == t)
throw std::runtime_error(&localtime failed&);
std::string buffer(64, ' ');
char* buffer_ptr = const_cast<char*>(buffer.c_str());
if (0 == strftime(buffer_ptr, buffer.size(), &%Y-%m-%d %T&, t))
throw std::runtime_error(&strftime failed&);
std::string temp(buffer_ptr, std::strlen(buffer_ptr)); //converted explicitly to std::string using ptr and len
return temp;
}
[/code]
Se executarmos as três versões desta forma:
[code language="cpp"]
for (const std::string& s : { timestamp(), timestamp2(), timestamp3() })
{
std::cout << s << &\n&;
std::cout << hex_rep(buffer_to_transmit(s)) << &\n&;
}
[/code]
Obteremos uma saída similar há:
2016-05-10 19:16:31
Size: 19 bytes
323031362D30352D31302031393A31363A3331
2016-05-10 19:16:31
Size: 64 bytes
323031362D30352D31302031393A31363A3331002020202020202020202020202020202020202020202020202020202020202020202020202020202020202020
2016-05-10 19:16:31
Size: 19 bytes
323031362D30352D31302031393A31363A3331
Onde as versões 1 e 3 serão os resultados esperados.
Abaixo uma compilação e execução do exemplo timestamp:

Fontes:
https://github.com/SimplyCpp/examples/tree/master/c_cpp_string
Comentários
Postar um comentário