From 1529e635fcf8fc55ecad6ac0a6abea7f5703cb60 Mon Sep 17 00:00:00 2001 From: caheckman <48068198+caheckman@users.noreply.github.com> Date: Mon, 20 Apr 2020 17:31:02 -0400 Subject: [PATCH] Converted StringManager to use vectors --- .../src/decompile/cpp/ghidra_arch.cc | 33 ++++--- .../src/decompile/cpp/ghidra_arch.hh | 3 +- .../src/decompile/cpp/ifacedecomp.cc | 17 ++++ .../src/decompile/cpp/ifacedecomp.hh | 5 + .../Decompiler/src/decompile/cpp/printc.cc | 19 ++-- .../src/decompile/cpp/string_ghidra.cc | 11 ++- .../src/decompile/cpp/string_ghidra.hh | 2 +- .../src/decompile/cpp/stringmanage.cc | 99 ++++++++----------- .../src/decompile/cpp/stringmanage.hh | 16 ++- .../app/decompiler/DecompileCallback.java | 59 +++++++++-- .../app/decompiler/DecompileProcess.java | 8 +- 11 files changed, 161 insertions(+), 111 deletions(-) diff --git a/Ghidra/Features/Decompiler/src/decompile/cpp/ghidra_arch.cc b/Ghidra/Features/Decompiler/src/decompile/cpp/ghidra_arch.cc index f688fa739b..90e1852882 100644 --- a/Ghidra/Features/Decompiler/src/decompile/cpp/ghidra_arch.cc +++ b/Ghidra/Features/Decompiler/src/decompile/cpp/ghidra_arch.cc @@ -19,6 +19,7 @@ #include "ghidra_translate.hh" #include "typegrp_ghidra.hh" #include "comment_ghidra.hh" +#include "string_ghidra.hh" #include "cpool_ghidra.hh" #include "inject_ghidra.hh" @@ -346,6 +347,12 @@ void ArchitectureGhidra::buildCommentDB(DocumentStorage &store) commentdb = new CommentDatabaseGhidra(this); } +void ArchitectureGhidra::buildStringManager(DocumentStorage &store) + +{ + stringManager = new GhidraStringManager(this,2048); +} + void ArchitectureGhidra::buildConstantPool(DocumentStorage &store) { @@ -615,7 +622,7 @@ void ArchitectureGhidra::getBytes(uint1 *buf,int4 size,const Address &inaddr) readResponseEnd(sin); } -uint4 ArchitectureGhidra::getStringData(uint1 *buf,const Address &addr,Datatype *ct,int4 maxBytes) +void ArchitectureGhidra::getStringData(vector &buffer,const Address &addr,Datatype *ct,int4 maxBytes) { sout.write("\000\000\001\004",4); @@ -633,32 +640,28 @@ uint4 ArchitectureGhidra::getStringData(uint1 *buf,const Address &addr,Datatype readToResponse(sin); int4 type = readToAnyBurst(sin); - uint4 size = 0; if (type == 12) { int4 c = sin.get(); - size ^= (c-0x20); + uint4 size = (c-0x20); c = sin.get(); size ^= ((c-0x20)<<6); + buffer.reserve(size); uint1 *dblbuf = new uint1[size * 2]; sin.read((char *)dblbuf,size*2); for (int4 i=0; i < size; i++) { - buf[i] = ((dblbuf[i*2]-'A') << 4) | (dblbuf[i*2 + 1]-'A'); + buffer.push_back(((dblbuf[i*2]-'A') << 4) | (dblbuf[i*2 + 1]-'A')); } delete [] dblbuf; + type = readToAnyBurst(sin); + if (type != 13) + throw JavaError("alignment","Expecting byte alignment end"); + type = readToAnyBurst(sin); } - else if ((type&1)==1) { - ostringstream errmsg; - errmsg << "GHIDRA has no string in the loadimage at " << addr.getShortcut(); - addr.printRaw(errmsg); - throw DataUnavailError(errmsg.str()); + if ((type&1)==1) { + // Leave the buffer empty } else - throw JavaError("alignment","Expecting bytes or end of query response"); - type = readToAnyBurst(sin); - if (type != 13) - throw JavaError("alignment","Expecting byte alignment end"); - readResponseEnd(sin); - return size; + throw JavaError("alignment","Expecting end of query response"); } /// \brief Retrieve p-code to inject for a specific context diff --git a/Ghidra/Features/Decompiler/src/decompile/cpp/ghidra_arch.hh b/Ghidra/Features/Decompiler/src/decompile/cpp/ghidra_arch.hh index 66d17db272..7eee505f52 100644 --- a/Ghidra/Features/Decompiler/src/decompile/cpp/ghidra_arch.hh +++ b/Ghidra/Features/Decompiler/src/decompile/cpp/ghidra_arch.hh @@ -74,6 +74,7 @@ class ArchitectureGhidra : public Architecture { virtual PcodeInjectLibrary *buildPcodeInjectLibrary(void); virtual void buildTypegrp(DocumentStorage &store); virtual void buildCommentDB(DocumentStorage &store); + virtual void buildStringManager(DocumentStorage &store); virtual void buildConstantPool(DocumentStorage &store); virtual void buildContext(DocumentStorage &store); virtual void buildSpecFile(DocumentStorage &store); @@ -124,7 +125,7 @@ public: bool getSendParamMeasures(void) const { return sendParamMeasures; } ///< Get the current setting for emitting parameter info - virtual uint4 getStringData(uint1 *buf,const Address &addr,Datatype *ct,int4 maxBytes); + virtual void getStringData(vector &buffer,const Address &addr,Datatype *ct,int4 maxBytes); virtual void printMessage(const string &message) const; static void segvHandler(int4 sig); ///< Handler for a segment violation (SIGSEGV) signal diff --git a/Ghidra/Features/Decompiler/src/decompile/cpp/ifacedecomp.cc b/Ghidra/Features/Decompiler/src/decompile/cpp/ifacedecomp.cc index a37afdc4bd..241c495ec5 100644 --- a/Ghidra/Features/Decompiler/src/decompile/cpp/ifacedecomp.cc +++ b/Ghidra/Features/Decompiler/src/decompile/cpp/ifacedecomp.cc @@ -127,6 +127,7 @@ void IfaceDecompCapability::registerCommands(IfaceStatus *status) status->registerCom(new IfcCallFixup(),"fixup","call"); status->registerCom(new IfcCallOtherFixup(),"fixup","callother"); status->registerCom(new IfcVolatile(),"volatile"); + status->registerCom(new IfcReadonly(),"readonly"); status->registerCom(new IfcPreferSplit(),"prefersplit"); status->registerCom(new IfcStructureBlocks(),"structure","blocks"); status->registerCom(new IfcAnalyzeRange(), "analyze","range"); @@ -2304,6 +2305,22 @@ void IfcVolatile::execute(istream &s) *status->optr << "Successfully marked range as volatile" << endl; } +void IfcReadonly::execute(istream &s) + +{ + int4 size = 0; + if (dcp->conf == (Architecture *)0) + throw IfaceExecutionError("No load image present"); + Address addr = parse_machaddr(s,size,*dcp->conf->types); // Read required address + + if (size == 0) + throw IfaceExecutionError("Must specify a size"); + Range range( addr.getSpace(), addr.getOffset(), addr.getOffset() + (size-1)); + dcp->conf->symboltab->setPropertyRange(Varnode::readonly,range); + + *status->optr << "Successfully marked range as readonly" << endl; +} + void IfcPreferSplit::execute(istream &s) { // Mark a particular storage location as something we would prefer to split diff --git a/Ghidra/Features/Decompiler/src/decompile/cpp/ifacedecomp.hh b/Ghidra/Features/Decompiler/src/decompile/cpp/ifacedecomp.hh index 95c1622381..f4548167d7 100644 --- a/Ghidra/Features/Decompiler/src/decompile/cpp/ifacedecomp.hh +++ b/Ghidra/Features/Decompiler/src/decompile/cpp/ifacedecomp.hh @@ -531,6 +531,11 @@ public: virtual void execute(istream &s); }; +class IfcReadonly : public IfaceDecompCommand { +public: + virtual void execute(istream &s); +}; + class IfcPreferSplit : public IfaceDecompCommand { public: virtual void execute(istream &s); diff --git a/Ghidra/Features/Decompiler/src/decompile/cpp/printc.cc b/Ghidra/Features/Decompiler/src/decompile/cpp/printc.cc index e27184bed3..c032f2f5d0 100644 --- a/Ghidra/Features/Decompiler/src/decompile/cpp/printc.cc +++ b/Ghidra/Features/Decompiler/src/decompile/cpp/printc.cc @@ -1216,20 +1216,23 @@ bool PrintC::doEmitWideCharPrefix(void) const bool PrintC::printCharacterConstant(ostream &s,const Address &addr,Datatype *charType) const { - const uint1 *buffer; StringManager *manager = glb->stringManager; - try { - buffer = manager->getStringData(addr, charType); - } catch(DataUnavailError &err) { + + // Retrieve UTF8 version of string + const vector &buffer(manager->getStringData(addr, charType)); + if (buffer.empty()) return false; - } if (doEmitWideCharPrefix() && charType->getSize() > 1) s << 'L'; // Print symbol indicating wide character s << '"'; - if (!escapeCharacterData(s,buffer,manager->getMaximumBytes(),charType->getSize(),glb->translate->isBigEndian())) + if (!escapeCharacterData(s,buffer.data(),buffer.size(),1,glb->translate->isBigEndian())) s << "...\" /* TRUNCATED STRING LITERAL */"; - else - s << '"'; + else { + if (buffer.size() > manager->getMaximumBytes()) + s << "...\" /* TRUNCATED STRING LITERAL */"; + else + s << '"'; + } return true; } diff --git a/Ghidra/Features/Decompiler/src/decompile/cpp/string_ghidra.cc b/Ghidra/Features/Decompiler/src/decompile/cpp/string_ghidra.cc index 3252838966..751e8ccc25 100644 --- a/Ghidra/Features/Decompiler/src/decompile/cpp/string_ghidra.cc +++ b/Ghidra/Features/Decompiler/src/decompile/cpp/string_ghidra.cc @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#include "ghidra_string.hh" +#include "string_ghidra.hh" GhidraStringManager::GhidraStringManager(ArchitectureGhidra *g,int4 max) : StringManager(max) @@ -28,14 +28,15 @@ GhidraStringManager::~GhidraStringManager(void) delete [] testBuffer; } -const uint1 *GhidraStringManager::getStringData(const Address &addr,Datatype *charType) +const vector &GhidraStringManager::getStringData(const Address &addr,Datatype *charType) { - map::iterator iter; + map >::iterator iter; iter = stringMap.find(addr); if (iter != stringMap.end()) return (*iter).second; - int4 size = glb->getStringData(testBuffer, addr, charType, maximumBytes); - return mapBuffer(addr, testBuffer, size); + vector &buffer(stringMap[addr]); + glb->getStringData(buffer, addr, charType, maximumBytes); + return buffer; } diff --git a/Ghidra/Features/Decompiler/src/decompile/cpp/string_ghidra.hh b/Ghidra/Features/Decompiler/src/decompile/cpp/string_ghidra.hh index cd943db402..74c5cfe3ef 100644 --- a/Ghidra/Features/Decompiler/src/decompile/cpp/string_ghidra.hh +++ b/Ghidra/Features/Decompiler/src/decompile/cpp/string_ghidra.hh @@ -33,7 +33,7 @@ class GhidraStringManager : public StringManager { public: GhidraStringManager(ArchitectureGhidra *g,int4 max); ///< Constructor virtual ~GhidraStringManager(void); - virtual const uint1 *getStringData(const Address &addr,Datatype *charType); + virtual const vector &getStringData(const Address &addr,Datatype *charType); }; #endif diff --git a/Ghidra/Features/Decompiler/src/decompile/cpp/stringmanage.cc b/Ghidra/Features/Decompiler/src/decompile/cpp/stringmanage.cc index de95db2c77..7a8c7784c0 100644 --- a/Ghidra/Features/Decompiler/src/decompile/cpp/stringmanage.cc +++ b/Ghidra/Features/Decompiler/src/decompile/cpp/stringmanage.cc @@ -16,21 +16,6 @@ #include "stringmanage.hh" #include "architecture.hh" -/// Before calling, we must check that there is no other buffer stored at the address. -/// \param addr is the Address to store the buffer at -/// \param buf is the buffer to be copied into storage -/// \param size is the number of bytes in the buffer -/// \return the new permanent copy of the buffer -const uint1 *StringManager::mapBuffer(const Address &addr,const uint1 *buf,int4 size) - -{ - uint1 *storeBuf = new uint1[size + 1]; - stringMap[addr] = storeBuf; - memcpy(storeBuf,buf,size); - storeBuf[size] = 0; - return storeBuf; -} - /// \param max is the maximum number of bytes to allow in a decoded string StringManager::StringManager(int4 max) @@ -44,16 +29,6 @@ StringManager::~StringManager(void) clear(); } -void StringManager::clear(void) - -{ - map::iterator iter; - - for(iter=stringMap.begin();iter!=stringMap.end();++iter) { - delete [] (*iter).second; - } -} - /// Encode the given unicode codepoint as UTF8 (1, 2, 3, or 4 bytes) and /// write the bytes to the stream. /// \param s is the output stream @@ -103,14 +78,8 @@ void StringManager::writeUtf8(ostream &s,int4 codepoint) bool StringManager::isString(const Address &addr,Datatype *charType) { - const uint1 *buffer = (const uint1 *)0; - try { - buffer = getStringData(addr,charType); - } - catch(DataUnavailError &err) { - return false; - } - return (buffer != (const uint1 *)0); + const vector &buffer(getStringData(addr,charType)); + return !buffer.empty(); } /// Write \ tag, with \ sub-tags. @@ -120,15 +89,14 @@ void StringManager::saveXml(ostream &s) const { s << "\n"; - map::const_iterator iter1; + map >::const_iterator iter1; for(iter1=stringMap.begin();iter1!=stringMap.end();++iter1) { s << "\n"; (*iter1).first.saveXml(s); - const uint1 *buf = (*iter1).second; + const vector &vec( (*iter1).second ); s << " \n" << setfill('0'); - for(int4 i=0;;++i) { - if (buf[i] == 0) break; - s << hex << setw(2) << (int4)buf[i]; + for(int4 i=0;vec.size();++i) { + s << hex << setw(2) << (int4)vec[i]; if (i%20 == 19) s << "\n "; } @@ -148,7 +116,7 @@ void StringManager::restoreXml(const Element *el,const AddrSpaceManager *m) iter = list.begin(); Address addr = Address::restoreXml(*iter, m); ++iter; - vector vec; + vector &vec(stringMap[addr]); istringstream is((*iter)->getContent()); int4 val; char c1, c2; @@ -174,7 +142,6 @@ void StringManager::restoreXml(const Element *el,const AddrSpaceManager *m) c1 = is.get(); c2 = is.get(); } - mapBuffer(addr,vec.data(),vec.size()); } /// \param buffer is the byte buffer @@ -300,49 +267,61 @@ StringManagerUnicode::~StringManagerUnicode(void) delete [] testBuffer; } -const uint1 *StringManagerUnicode::getStringData(const Address &addr,Datatype *charType) +const vector &StringManagerUnicode::getStringData(const Address &addr,Datatype *charType) { - map::iterator iter; + map >::iterator iter; iter = stringMap.find(addr); if (iter != stringMap.end()) return (*iter).second; + vector &vec(stringMap[addr]); // Allocate (initially empty) byte vector + int4 curBufferSize = 0; int4 charsize = charType->getSize(); bool foundTerminator = false; - do { - int4 amount = 32; // Grab 32 bytes of image at a time - uint4 newBufferSize = curBufferSize + amount; - if (newBufferSize > maximumBytes) { - newBufferSize = maximumBytes; - amount = newBufferSize - curBufferSize; - if (amount == 0) break; - } - glb->loader->loadFill(testBuffer+curBufferSize,amount,addr + curBufferSize); - foundTerminator = hasCharTerminator(testBuffer+curBufferSize,amount,charsize); - curBufferSize = newBufferSize; - } while (!foundTerminator); + try { + do { + int4 amount = 32; // Grab 32 bytes of image at a time + uint4 newBufferSize = curBufferSize + amount; + if (newBufferSize > maximumBytes) { + newBufferSize = maximumBytes; + amount = newBufferSize - curBufferSize; + if (amount == 0) + break; + } + glb->loader->loadFill(testBuffer + curBufferSize, amount, + addr + curBufferSize); + foundTerminator = hasCharTerminator(testBuffer + curBufferSize, amount, + charsize); + curBufferSize = newBufferSize; + } while (!foundTerminator); + } catch (DataUnavailError &err) { + return vec; // Return the empty buffer + } - const uint1 *resBuffer; if (charsize == 1) { if (!isCharacterConstant(testBuffer,curBufferSize,charsize)) - return (const uint1 *)0; - resBuffer = mapBuffer(addr,testBuffer,curBufferSize); + return vec; // Return the empty buffer + vec.reserve(curBufferSize); + vec.assign(testBuffer,testBuffer+curBufferSize); } else { // We need to translate to UTF8 ostringstream s; if (!writeUnicode(s, testBuffer, curBufferSize, charsize)) - return (const uint1 *)0; + return vec; // Return the empty buffer string resString = s.str(); int4 newSize = resString.size(); if (newSize > maximumBytes) newSize = maximumBytes; - resBuffer = mapBuffer(addr,(const uint1 *)resString.c_str(),newSize); + vector &vec(stringMap[addr]); + vec.reserve(newSize); + const uint1 *ptr = (const uint1 *)resString.c_str(); + vec.assign(ptr,ptr+newSize); } - return resBuffer; + return vec; } /// If the string is encoded in UTF8 or ASCII, we get (on average) a bit of check diff --git a/Ghidra/Features/Decompiler/src/decompile/cpp/stringmanage.hh b/Ghidra/Features/Decompiler/src/decompile/cpp/stringmanage.hh index 0f107e3b49..79207966df 100644 --- a/Ghidra/Features/Decompiler/src/decompile/cpp/stringmanage.hh +++ b/Ghidra/Features/Decompiler/src/decompile/cpp/stringmanage.hh @@ -30,27 +30,25 @@ class Architecture; /// Stores the decoded string until its needed for presentation. class StringManager { protected: - map stringMap; ///< Map from address to string (in UTF8 format) - int4 maximumBytes; ///< Maximum bytes (in UTF8 encoding) allowed - - const uint1 *mapBuffer(const Address &addr,const uint1 *buf,int4 size); ///< Move a decoded buffer into storage + map > stringMap; ///< Map from address to string (in UTF8 format) + int4 maximumBytes; ///< Maximum bytes (in UTF8 encoding) allowed public: StringManager(int4 max); ///< Constructor virtual ~StringManager(void); ///< Destructor int4 getMaximumBytes(void) const { return maximumBytes; } ///< Return the maximum bytes allowed in a string decoding - void clear(void); ///< Clear out any cached strings + void clear(void) { stringMap.clear(); } ///< Clear out any cached strings bool isString(const Address &addr,Datatype *charType); // Determine if data at the given address is a string /// \brief Retrieve string data at the given address as a UTF8 byte array /// - /// If the address does not represent string data, null is returned. Otherwise, + /// If the address does not represent string data, a zero length vector is returned. Otherwise, /// the string data is fetched, converted to a UTF8 encoding, cached and returned. /// \param addr is the given address /// \param charType is a character data-type indicating the encoding - /// \return the byte array of UTF8 data (or null) - virtual const uint1 *getStringData(const Address &addr,Datatype *charType)=0; + /// \return the byte array of UTF8 data + virtual const vector &getStringData(const Address &addr,Datatype *charType)=0; void saveXml(ostream &s) const; ///< Save cached strings to a stream as XML void restoreXml(const Element *el,const AddrSpaceManager *m); ///< Restore string cache from XML @@ -72,7 +70,7 @@ public: StringManagerUnicode(Architecture *g,int4 max); ///< Constructor virtual ~StringManagerUnicode(void); - virtual const uint1 *getStringData(const Address &addr,Datatype *charType); + virtual const vector &getStringData(const Address &addr,Datatype *charType); bool isCharacterConstant(const uint1 *buf,int4 size,int4 charsize) const; ///< Return \b true if buffer looks like unicode bool writeUnicode(ostream &s,uint1 *buffer,int4 size,int4 charsize); ///< Write unicode byte array to stream (as UTF8) }; diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompileCallback.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompileCallback.java index 24b7e491b1..29821ac372 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompileCallback.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompileCallback.java @@ -1181,6 +1181,35 @@ public class DecompileCallback { return listing.getFunctionAt(addr); } + /** + * Return true if there are no "replacement" characters in the string + * @param string is the string to test + * @return true if no replacements + */ + private boolean isValidChars(String string) { + char replaceChar = '\ufffd'; + for (int i = 0; i < string.length(); ++i) { + char c = string.charAt(i); + if (c == replaceChar) { + return false; + } + } + return true; + } + + /** + * Check for a string at an address and return a UTF8 encoded byte array. + * If there is already data present at the address, use this to determine the + * string encoding. Otherwise use the data-type info passed in to determine the encoding. + * Check that the bytes at the address represent a valid string encoding that doesn't + * exceed the maximum byte limit passed in. Return null if the string is invalid. + * Return the string translated into a UTF8 byte array otherwise. A (valid) empty + * string is returned as a zero length array. + * @param addrString is the XML encoded address and maximum byte limit + * @param dtName is the name of a character data-type + * @param dtId is the id associated with the character data-type + * @return the UTF8 encoded byte array or null + */ public byte[] getStringData(String addrString, String dtName, String dtId) { Address addr; int maxBytes; @@ -1199,9 +1228,21 @@ public class DecompileCallback { Settings settings = SettingsImpl.NO_SETTINGS; AbstractStringDataType dataType = null; if (data != null) { - settings = data; if (data.getDataType() instanceof AbstractStringDataType) { + settings = data; dataType = (AbstractStringDataType) data.getDataType(); + int len = data.getLength(); + if (len > 0) { + long diff = addr.subtract(data.getAddress()) * + addr.getAddressSpace().getAddressableUnitSize(); + if (diff < 0 || diff >= len) { + return null; + } + len -= diff; + if (len < maxBytes) { + maxBytes = len; + } + } } } if (dataType == null) { @@ -1228,17 +1269,17 @@ public class DecompileCallback { } } MemoryBufferImpl buf = new MemoryBufferImpl(program.getMemory(), addr, 64); - Object value = dataType.getValue(buf, settings, maxBytes); - if (!(value instanceof String)) { + StringDataInstance stringInstance = dataType.getStringDataInstance(buf, settings, maxBytes); + int len = stringInstance.getStringLength(); + if (len < 0 || len > maxBytes) { return null; } - String stringVal = (String) value; - byte[] res = stringVal.getBytes(utf8Charset); - if (res.length > maxBytes) { - byte[] trim = new byte[maxBytes]; - System.arraycopy(res, 0, trim, 0, maxBytes); + + String stringVal = stringInstance.getStringValue(); + if (!isValidChars(stringVal)) { + return null; } - return res; + return stringVal.getBytes(utf8Charset); } //================================================================================================== diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompileProcess.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompileProcess.java index 628c15d9f8..8226533c6e 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompileProcess.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompileProcess.java @@ -789,19 +789,21 @@ public class DecompileProcess { String dtId = readQueryString(); byte[] res = callback.getStringData(addr, dtName, dtId); write(query_response_start); - if ((res != null) && (res.length > 0)) { - int sz = res.length; + if (res != null) { + int sz = res.length + 1; // We add a null terminator character int sz1 = (sz & 0x3f) + 0x20; sz >>>= 6; int sz2 = (sz & 0x3f) + 0x20; write(byte_start); write(sz1); write(sz2); - byte[] dblres = new byte[res.length * 2]; + byte[] dblres = new byte[res.length * 2 + 2]; for (int i = 0; i < res.length; i++) { dblres[i * 2] = (byte) (((res[i] >> 4) & 0xf) + 65); dblres[i * 2 + 1] = (byte) ((res[i] & 0xf) + 65); } + dblres[res.length * 2] = 65; // Adding null terminator + dblres[res.length * 2 + 1] = 65; write(dblres); write(byte_end); }