From 48b21d4330c072bdd6e8d3e2e3112228cb3f827a Mon Sep 17 00:00:00 2001 From: Bradley Fernandez Date: Wed, 16 Sep 2026 10:39:58 -0400 Subject: [PATCH] Fixed issues with attaching/detaching in OpenOCD, finished testing --- core/CMakeLists.txt | 2 + core/adapters/gdbadapter.cpp | 2 + core/adapters/openocdadapter.cpp | 419 ++++++++++++++++++ core/adapters/openocdadapter.h | 85 ++++ core/debugger.cpp | 2 + .../OpenOCD_Test.elf | Bin 0 -> 33120 bytes 6 files changed, 510 insertions(+) create mode 100644 core/adapters/openocdadapter.cpp create mode 100644 core/adapters/openocdadapter.h create mode 100755 test/binaries/OpenOCD-STM32-NUCLEO-F446RE/OpenOCD_Test.elf diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index d9408621..122cf680 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -26,6 +26,8 @@ file(GLOB ADAPTER_SOURCES CONFIGURE_DEPENDS adapters/lldbadapter.h adapters/gdbadapter.cpp adapters/gdbadapter.h + adapters/openocdadapter.cpp + adapters/openocdadapter.h adapters/gdbmiconnector.cpp adapters/gdbmiconnector.h adapters/gdbmiadapter.cpp diff --git a/core/adapters/gdbadapter.cpp b/core/adapters/gdbadapter.cpp index 4e6ab3bb..c285b650 100644 --- a/core/adapters/gdbadapter.cpp +++ b/core/adapters/gdbadapter.cpp @@ -238,6 +238,8 @@ bool GdbAdapter::Connect(const std::string& server, std::uint32_t port) } m_socket->Close(); + delete m_socket; + m_socket = nullptr; std::this_thread::sleep_for(std::chrono::milliseconds(500)); } diff --git a/core/adapters/openocdadapter.cpp b/core/adapters/openocdadapter.cpp new file mode 100644 index 00000000..3045e629 --- /dev/null +++ b/core/adapters/openocdadapter.cpp @@ -0,0 +1,419 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#include "openocdadapter.h" + +#include +#include +#include +#include +#include +#include +#include +#ifndef WIN32 +#include +#include +#include +#include +#ifdef __APPLE__ +#include +#else +extern char** environ; +#endif +#endif + +using namespace BinaryNinja; +using namespace BinaryNinjaDebugger; + +namespace +{ + std::string ResolveOpenOCDExecutable(const std::string& executable) + { +#ifdef WIN32 + DWORD length = SearchPathA(nullptr, executable.c_str(), ".exe", 0, nullptr, nullptr); + if (length == 0) + return executable; + std::vector path(length + 1); + if (SearchPathA(nullptr, executable.c_str(), ".exe", static_cast(path.size()), path.data(), nullptr)) + return path.data(); +#else + // Absolute and explicitly relative paths should be used exactly as configured. + if (executable.find('/') != std::string::npos) + return executable; + + std::vector directories; + if (const char* path = std::getenv("PATH")) + { + std::string_view remaining(path); + while (true) + { + auto separator = remaining.find(':'); + auto directory = remaining.substr(0, separator); + directories.emplace_back(directory.empty() ? "." : directory); + if (separator == std::string_view::npos) + break; + remaining.remove_prefix(separator + 1); + } + } + +#ifdef __APPLE__ + // Applications launched from Finder do not inherit shell startup files. Cover + // the standard Homebrew and MacPorts locations used by terminal installations. + directories.insert(directories.end(), {"/opt/homebrew/bin", "/usr/local/bin", "/opt/local/bin"}); +#else + directories.insert(directories.end(), {"/usr/local/bin", "/usr/bin", "/snap/bin"}); +#endif + for (const auto& directory : directories) + { + auto candidate = directory + "/" + executable; + if (access(candidate.c_str(), X_OK) == 0) + return candidate; + } +#endif + return executable; + } + + bool LaunchError(const std::string& message, OpenOCDAdapter* adapter) + { + DebuggerEvent event; + event.type = LaunchFailureEventType; + event.data.errorData.shortError = "OpenOCD connection failed"; + event.data.errorData.error = message; + adapter->PostDebuggerEvent(event); + return false; + } + +#ifdef WIN32 + // Escape an argv element using the Windows C runtime command-line rules. + std::string QuoteArgument(const std::string& argument) + { + std::string result = "\""; + size_t slashes = 0; + for (char c : argument) + { + if (c == '\\') + { + ++slashes; + continue; + } + result.append(c == '"' ? slashes * 2 + 1 : slashes, '\\'); + slashes = 0; + result += c; + } + result.append(slashes * 2, '\\'); + return result + '"'; + } +#endif +} + +OpenOCDAdapter::OpenOCDAdapter(BinaryView* data) : GdbAdapter(data, false) +{ + m_socket = nullptr; + GenerateDefaultAdapterSettings(data); +} + +bool OpenOCDAdapter::StartOpenOCD() +{ + if (m_spawnedOpenOCD) + return false; + + auto settings = GetAdapterSettings(); + auto data = GetData(); + auto configuredExecutable = settings->Get("connect.openocdPath", data); + auto files = settings->Get>("connect.configFiles", data); + auto port = settings->Get("connect.port", data); + if (configuredExecutable.empty() || files.empty()) + return LaunchError("Specify an OpenOCD executable and at least one board or interface/target config file.", this); + auto executable = ResolveOpenOCDExecutable(configuredExecutable); + + std::vector arguments {executable, "-c", "gdb_port " + std::to_string(port)}; + auto searchPath = settings->Get("connect.searchPath", data); + if (!searchPath.empty()) + arguments.insert(arguments.end(), {"-s", searchPath}); + for (const auto& file : files) + { + if (file.empty()) + return LaunchError("OpenOCD config file names must not be empty.", this); + arguments.insert(arguments.end(), {"-f", file}); + } + // Initialize and halt without resetting or programming the board. + arguments.insert(arguments.end(), {"-c", "init; halt"}); +#ifdef WIN32 + std::string commandLine; + for (const auto& argument : arguments) + commandLine += QuoteArgument(argument) + " "; + STARTUPINFOA startup{}; + startup.cb = sizeof(startup); + if (!CreateProcessA(nullptr, commandLine.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, + nullptr, nullptr, &startup, &m_openocdProcess)) + return LaunchError("Unable to start OpenOCD executable '" + executable + "' (Windows error " + + std::to_string(GetLastError()) + "). Configure an absolute executable path if Binary Ninja cannot see your shell PATH.", this); +#else + std::vector argv; + for (auto& argument : arguments) + argv.push_back(argument.data()); + argv.push_back(nullptr); +#ifdef __APPLE__ + char** environment = *_NSGetEnviron(); +#else + char** environment = environ; +#endif + int error = posix_spawnp(&m_openocdPid, executable.c_str(), nullptr, nullptr, argv.data(), environment); + if (error != 0) + { + m_openocdPid = -1; + return LaunchError("Unable to start OpenOCD executable '" + executable + "': " + std::string(strerror(error)) + + ". Configure an absolute executable path if Binary Ninja cannot see your shell PATH.", this); + } +#endif + m_spawnedOpenOCD = true; + return true; +} + +void OpenOCDAdapter::StopOpenOCD() +{ + if (!m_spawnedOpenOCD) + return; +#ifdef WIN32 + if (WaitForSingleObject(m_openocdProcess.hProcess, 0) == WAIT_TIMEOUT) + { + TerminateProcess(m_openocdProcess.hProcess, 0); + WaitForSingleObject(m_openocdProcess.hProcess, 2000); + } + CloseHandle(m_openocdProcess.hThread); + CloseHandle(m_openocdProcess.hProcess); + m_openocdProcess = {}; +#else + // Reap before signalling so an already exited child is handled safely. + int status = 0; + pid_t result; + do { result = waitpid(m_openocdPid, &status, WNOHANG); } while (result < 0 && errno == EINTR); + if (result == 0) + { + kill(m_openocdPid, SIGTERM); + for (size_t i = 0; i < 40 && result == 0; ++i) + { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + do { result = waitpid(m_openocdPid, &status, WNOHANG); } while (result < 0 && errno == EINTR); + } + if (result == 0) + { + kill(m_openocdPid, SIGKILL); + while (waitpid(m_openocdPid, &status, 0) < 0 && errno == EINTR) {} + } + } + m_openocdPid = -1; +#endif + m_spawnedOpenOCD = false; +} + +void OpenOCDAdapter::JoinExecutionThread() +{ + if (m_executionThread.joinable()) + m_executionThread.join(); +} + +OpenOCDAdapter::~OpenOCDAdapter() +{ + if (m_rspConnector && m_socket) + m_socket->Kill(); + JoinExecutionThread(); + delete m_rspConnector; + delete m_socket; + StopOpenOCD(); +} + +bool OpenOCDAdapter::Execute(const std::string&, const LaunchConfigurations&) +{ + return Connect({}, 0); +} + +bool OpenOCDAdapter::ExecuteWithArgs(const std::string&, const std::string&, const std::string&, + const LaunchConfigurations&) +{ + return Connect({}, 0); +} + +bool OpenOCDAdapter::Attach(std::uint32_t) +{ + // OpenOCD exposes a debug target rather than a host process list. Treat Binary + // Ninja's Attach action as a connection request and ignore its PID field. + return Connect({}, 0); +} + +std::vector OpenOCDAdapter::GetProcessList() +{ + // Binary Ninja requires a process-list selection before it calls Attach(). + // OpenOCD has one configured debug target rather than an OS process list, so + // provide a synthetic entry. Its ID is intentionally ignored by Attach(). + auto settings = GetAdapterSettings(); + auto data = GetData(); + auto host = settings->Get("connect.ipAddress", data); + auto port = settings->Get("connect.port", data); + return {DebugProcess(1, "OpenOCD target", host + ":" + std::to_string(port))}; +} + +bool OpenOCDAdapter::Connect(const std::string& server, std::uint32_t port) +{ + if (m_rspConnector || m_spawnedOpenOCD) + return LaunchError("An OpenOCD session is already active.", this); + + auto settings = GetAdapterSettings(); + auto data = GetData(); + auto host = settings->Get("connect.ipAddress", data); + auto configuredPort = settings->Get("connect.port", data); + if (configuredPort == 0 || configuredPort > 65535) + return LaunchError("The GDB port must be between 1 and 65535.", this); + if (settings->Get("connect.spawnOpenOCD", data)) + { + if (host != "127.0.0.1") + return LaunchError("Local OpenOCD startup requires IP address 127.0.0.1. Disable startup to connect remotely.", this); + if (!StartOpenOCD()) + return false; + } + + delete m_socket; + m_socket = nullptr; + m_registerInfo.clear(); + m_remoteArch.clear(); + InvalidateCache(); + + // The base adapter reads connect.ipAddress/connect.port from our settings and retries + // for 15 seconds, allowing OpenOCD time to initialize the debug probe. + try + { + if (GdbAdapter::Connect(server, port)) + { + if (m_resetOnNextConnect) + { + RunMonitorCommand("reset halt"); + m_resetOnNextConnect = false; + InvalidateCache(); + } + return true; + } + } + catch (const std::exception& error) + { + LaunchError(error.what(), this); + } + if (m_rspConnector && m_socket) + m_socket->Kill(); + delete m_rspConnector; + m_rspConnector = nullptr; + delete m_socket; + m_socket = nullptr; + StopOpenOCD(); + return false; +} + +bool OpenOCDAdapter::Go() +{ + // The debugger controller expects resume requests to return promptly and waits + // for AdapterStoppedEventType separately. GdbAdapter::Go waits synchronously for + // the stop reply, which prevents the controller's interrupt thread from acquiring + // its adapter lock when Pause, Detach, or Quit is requested. + JoinExecutionThread(); + m_executionThread = std::thread([this] { GdbAdapter::Go(); }); + return true; +} + +bool OpenOCDAdapter::StepInto() +{ + JoinExecutionThread(); + m_executionThread = std::thread([this] { GdbAdapter::StepInto(); }); + return true; +} + +bool OpenOCDAdapter::Detach() +{ + JoinExecutionThread(); + bool result = GdbAdapter::Detach(); + delete m_socket; + m_socket = nullptr; + StopOpenOCD(); + return result; +} + +bool OpenOCDAdapter::Quit() +{ + // DebuggerController implements Restart as Quit followed by Launch on the same + // adapter. Remember that transition so the new connection resets and halts the + // board. A standalone Quit still disconnects instead of sending an RSP process kill. + m_resetOnNextConnect = true; + return Detach(); +} + +std::vector OpenOCDAdapter::GetModuleList() +{ + if (!m_rspConnector) + return {}; + // Bare-metal firmware has no /proc//maps. Use the loaded image's link + // addresses so module-relative breakpoints resolve without Linux host I/O. + auto data = GetData(); + DebugModule module; + module.m_name = GetAdapterSettings()->Get("common.inputFile", data); + module.m_short_name = module.m_name; + module.m_address = m_start; + module.m_size = data->GetEnd() - m_start; + module.m_loaded = true; + return {module}; +} + +Ref OpenOCDAdapter::GetAdapterSettings() +{ + return OpenOCDAdapterType::GetAdapterSettings(); +} + +OpenOCDAdapterType::OpenOCDAdapterType() : DebugAdapterType("OpenOCD") {} +DebugAdapter* OpenOCDAdapterType::Create(BinaryView* data) { return new OpenOCDAdapter(data); } +bool OpenOCDAdapterType::IsValidForData(BinaryView* data) { return true; } +bool OpenOCDAdapterType::CanExecute(BinaryView* data) { return true; } +bool OpenOCDAdapterType::CanConnect(BinaryView* data) { return true; } + +void BinaryNinjaDebugger::InitOpenOCDAdapterType() +{ + static OpenOCDAdapterType type; + DebugAdapterType::Register(&type); +} + +Ref OpenOCDAdapterType::GetAdapterSettings() +{ + static Ref settings = RegisterAdapterSettings(); + return settings; +} + +Ref OpenOCDAdapterType::RegisterAdapterSettings() +{ + auto settings = Settings::Instance("OpenOCDAdapterSettings"); + settings->SetResourceId("openocd_adapter_settings"); + settings->RegisterSetting("common.inputFile", R"({"title":"Input File","type":"string","default":"", + "description":"Local firmware image used to identify the target module","uiSelectionAction":"file"})"); + settings->RegisterSetting("connect.ipAddress", R"({"title":"IP Address","type":"string","default":"127.0.0.1", + "description":"IPv4 address of the OpenOCD GDB server"})"); + settings->RegisterSetting("connect.port", R"({"title":"GDB Port","type":"number","default":3333, + "minValue":1,"maxValue":65535,"description":"OpenOCD GDB server port; config files must use the same port"})"); + settings->RegisterSetting("connect.spawnOpenOCD", R"({"title":"Start OpenOCD","type":"boolean","default":true, + "description":"Start a local OpenOCD process when connecting and stop it when disconnecting"})"); + settings->RegisterSetting("connect.openocdPath", R"({"title":"OpenOCD Executable","type":"string","default":"openocd", + "description":"Executable path or name on PATH; an absolute path also works when a GUI application does not inherit the shell PATH","uiSelectionAction":"file"})"); + settings->RegisterSetting("connect.configFiles", R"({"title":"OpenOCD Config Files","type":"array", + "sorted":false,"default":["openocd.cfg"], + "description":"Ordered -f arguments: board config, or interface config followed by target config"})"); + settings->RegisterSetting("connect.searchPath", R"({"title":"OpenOCD Script Directory","type":"string","default":"", + "description":"Optional script search directory passed with -s","uiSelectionAction":"directory"})"); + return settings; +} diff --git a/core/adapters/openocdadapter.h b/core/adapters/openocdadapter.h new file mode 100644 index 00000000..a60f859e --- /dev/null +++ b/core/adapters/openocdadapter.h @@ -0,0 +1,85 @@ +/* +Copyright 2020-2026 Vector 35 Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#pragma once +#include "../debugadapter.h" +#include "../debugadaptertype.h" +#include "gdbadapter.h" + +#ifdef WIN32 +#include +#else +#include +#endif +#include + +namespace BinaryNinjaDebugger { + + // OpenOCD's GDB server is roughly the same as normal GDB's RSP, so all handling in GdbAdapter + // (breakpoints, registers, memory, stepping, ...) works the same here. The changes are to the + // GDB port (defaults to 3333), and users typically expecting the debugger to start OpenOCD for + // them rather than having to launch it by hand first. So, when enabled, Connect() spawns a local + // `openocd` process and then hands off to GdbAdapter::Connect() to do the actual RSP handshake. + class OpenOCDAdapter : public GdbAdapter + { + bool m_spawnedOpenOCD = false; + bool m_resetOnNextConnect = false; + std::thread m_executionThread; +#ifdef WIN32 + PROCESS_INFORMATION m_openocdProcess{}; +#else + pid_t m_openocdPid = -1; +#endif + + bool StartOpenOCD(); + void StopOpenOCD(); + void JoinExecutionThread(); + + public: + OpenOCDAdapter(BinaryView* data); + ~OpenOCDAdapter(); + + bool Execute(const std::string& path, const LaunchConfigurations& configs) override; + bool ExecuteWithArgs(const std::string& path, const std::string& args, const std::string& workingDir, + const LaunchConfigurations& configs) override; + bool Attach(std::uint32_t pid) override; + std::vector GetProcessList() override; + bool Connect(const std::string& server, std::uint32_t port) override; + bool Go() override; + bool StepInto() override; + bool Detach() override; + bool Quit() override; + std::vector GetModuleList() override; + + Ref GetAdapterSettings() override; + }; + + + class OpenOCDAdapterType: public DebugAdapterType + { + static Ref RegisterAdapterSettings(); + + public: + OpenOCDAdapterType(); + virtual DebugAdapter* Create(BinaryNinja::BinaryView* data); + virtual bool IsValidForData(BinaryNinja::BinaryView* data); + virtual bool CanExecute(BinaryNinja::BinaryView* data); + virtual bool CanConnect(BinaryNinja::BinaryView* data); + static Ref GetAdapterSettings(); + }; + + void InitOpenOCDAdapterType(); +} // namespace BinaryNinjaDebugger diff --git a/core/debugger.cpp b/core/debugger.cpp index d37ef548..918a7ed2 100644 --- a/core/debugger.cpp +++ b/core/debugger.cpp @@ -16,6 +16,7 @@ limitations under the License. #include #include "adapters/gdbadapter.h" +#include "adapters/openocdadapter.h" #include "adapters/gdbmiadapter.h" #include "adapters/lldbadapter.h" #include "adapters/corelliumadapter.h" @@ -52,6 +53,7 @@ void InitDebugAdapterTypes() InitCorelliumAdapterType(); InitGdbAdapterType(); + InitOpenOCDAdapterType(); InitGdbMiAdapterType(); InitLldbAdapterType(); InitEsrevenAdapterType(); diff --git a/test/binaries/OpenOCD-STM32-NUCLEO-F446RE/OpenOCD_Test.elf b/test/binaries/OpenOCD-STM32-NUCLEO-F446RE/OpenOCD_Test.elf new file mode 100755 index 0000000000000000000000000000000000000000..cb552b111e8db6a5b82b1c99e082840e73138d25 GIT binary patch literal 33120 zcmeHwd3A>>mXaVq|T^rFfsjX$Tn35B|xsvWzIVc-}Bj$z;!299Ci z7zU1E;1~vuVc-}Bj$z;!299Ci7zX}3G2jc)TK`iG{=B2;I+U;9|9^aHy!^k;UH4-? zsZX6Y-zjgY@zx#<?H#XmoLPs=^;&CK+h?~hxe<*WEhv;%_S>hQsFVV!~3 zKzF(=FM_L@^UHk8gU2;(u#UG*^l!9Iwob85wKiFIQypOetwQ)u+91Tso?j)zp}!Gg zC1n|xWd(oq3IximmM6}}!xz}vOpoRp5JIz>0mcaOC zdNdDqgL+MY@u?b>J<)-h??QEdWJ4a+1DD|zycEz|&4DSrH-X6KQ$II1Z^J7f6D^q2 ziJ`0U=6gpCcYwjLUxKIgAcA=>$IGMVNbr|$N9N_ejd+AfS@igYb-Gxw;@lPf)#%^^ z;a|ON#Vc3zuE?$j5bLZnRPSWkpIw`w; zopoIByp69rNr>hZ79{{=T=-jb&%TWW8g*DXkvhj9z?LtJ!L!Gs{u&JZN>q^-ct!a5 zmC50tKoyJAx%?j7W%&V2tANxe@s#akP4o)9$U4mukH?!?+^$)+3dj+g=TQL|@>@;* zWr4u*7^v@G=?ermf~@|QVBmyyP|<&CbMw9x7{2*ZzAr!B0w;U_?PK>RvN_piu4F-Sm+b;%Le4B;CXW8Y@{+`^^*!TFy7%c3p!e=|dJ6mjc-xQF}cVn`|=TR^;?nV|<< z8xY@P_ZEBHurYa{h(;EbzSE$YWi{aJKR+e#!Sarq|~oU;&Y2 zO1+co|B-8MWC}jbIK0zGbzaW>e2Fo97h|=?rJ>`f^fre1AmeH`<3G)FM-`NQh-+3E zw*SG{80WD*$i2ORb9NKciR=0PY38v@LX@+fd%J|O(axn6o@tItKg;&+&AI^Px_vc``%Z z$1vOgZ3N!t{(s9bEO4D4aBn|kTD*g?*UbEU4Rd3L;kF8-2}tGaSu;JWkdEEC;z}w^L6g0h3l+f`T9Ngc_G99 zzqp6PEZtd_nqTmI|IDyG!W6leu`$i}KE_p$>u=!^&*%EzWIjL2Job#FGUwmLRQoE! z^B$(l*BSCB7;j%^K?1N0BE4lPs z9_2jl^UutCAL6#>a-Z+z`afj|uVqSYW7z(QYyLCiYLuz=Iqv@{hUzKC(k+bn`0V?wKg0Q* z-1DCp&lmC>f5zAgFc*EDDRMny;0>$?e#7y|q?=Mk&o3FzMec0_^YepT^B}|dC&@Y7XOg+=DyG^AOry6kEh0SYhZrYsVY+{Warl02 z`vHbO3p<6=4{^@jJo*DXuM@cce%5B)obzFx`!Bd=JL7gW z)2Nd%*~_y?GH*P`+;|O-xPc+bFn#+N^UF36(3ps0aT zL9reAL2(K5_Vt)`P<$AW2St+K_XCqbaVy`yfS!ZmBCh{DV=ThhyAaq2if=OiUv@T) zo?<*~MxQ~^%P`;8L-`i~=Rxst#?NZ5|4oMWqfCpB@~j_c8U--lpjabm%~baD`|CjE zpjgS;`+MNJpxDNvJjKvH0eCPYNE}dsD=5xp&ioQ2F(@7du7aY(@IStp;5o{&^DE}q z<;;ywgMWhJV@&-k7_0X)-`>HPoMg!Nokn%`L7xQ06QFERv@pirgqlI|dWPXi)Pbeu zQQAZFet>cE3C7#SYba-yadjQ@zzrB3S#Wxv2Wvv&-=DfgKzk5BpN;9yM|c#N#DC6 zTMW$_i>P*ThH!Y=!<5;67irq`P!{ODil)46KUgA=EYesz{*e$|{2>wRHSI){!y`1Q zi#iDp3lC7MH(9?UnZMdvLs-1dx}8$*v2G+Gywy65(D!lc>olX!TK_@3c(=u{f6ijy zKW{PgU$7Ybdn|_kUW*QwlJk>(K){`HH+6m5?+L^+9w6>K zYX{-C?Og<7^n3!UjX^IV?R4ox&rXxQ_W-g@6QJZ`YI z5#DdK_EYLs>l?&<@3)T9P#>^%5z~Ic$`CT|u#Qq^pR|eu-`&=8l=(U98)S7pZ$*h# z_gU8v9AC9gp%K1j{hG-8UF)+XJ`Y;!2|o{6S>nZCSijzc$D`Ii5V{_-{++5kV||>M z_xILL(w@&+Z3OOBzFjnvxB8AJ)Lrd+lFHugTS?8|<69BJ<9c75_~LfoBV+|W=z9gB z=R>|TsOD#VJ7`9C`DO^DyM6r~czn%QB@KVSZwHO{fN!4g{ypE7G{O&kClStn`Q;5?~08!x2 z3WwWEvm>HCF*7qUJ~CVzpPrg)FO~BhvE=;0YBrS)56{fBC#R=s)v4NCdwOJId}glN zJ~J_YXdE?1rYFO5HT*x>5gS2+@c3voJUKESt}fK7vs1$pwdv`Jx$vQ>`8IxQ8=IbO z^PwAzlghsUR?f4vi!J2Lmz0Q|)G!S+KVBOWR+4WpVEe+~0!$VEa-4v$ZT zM@S;<(dyXn{6x*J9hs@lg%4AJ%bu%^LQNw95pv$40i1W(+Og_zZGM)Upq4#Vy#zS3 zFRsqcL8j0YRcv%JH$Nl$5dljCd=y_hDG)d}uQ7+A`P!if=qgyk_wp-Ek@bJY|dt!R(P)Cf%svVx6 zJQ!00Gemp69m|dg9^he5HdP)#^^O=i>rW;%0?O%9*-i|m>{7aHmy4D3?nDj)00aUv zpB)TsY10Wn*A|KrG?iQ;nXKgPbfJ{Z4Gp5-$U?Nx))A}ZcA_|+EhX*U*;2MImu6@s z%JKh@5+28xxk8ie3BhtdepiL5x@gO^IF@D7j^!H6*F0Qd}a50!k~YFm;(wGEdS)p+1#PX7h=hnq9OT zAnYw=9UKtbr229+PeuAIZ6H4568P)4`KD>qMKdsHlpIJDK|(OaP;#f8DW=mQqN2&` z$yC}-4i$<+#NRo|vNV_t@$>G7?Bi>Fn%fP^4^tHMIOm96A_O+(ZvY05#7_JveuGdbZ}(hN>B}erl)! zU4hYh0D6un+5id?aH(ENH+j_dP%)J*+I`S2>A_SsL98F|h_PDMe5hLO&3LXtTUWf2 z%M~#+NnLViumnw*EN6G8SyDnQkS3N>r2r zQhJx2ER{nTU@*NWm+gbOE|#)GgP~3FaA!EOiR7@Zq3ek{K&649A}M_+M0Xc9Svw+) zgLN%6WbKh(O!sF?W$+BtQ3fi(4j~hhB9X5fDu;4J49WkY&6`7D7lI9zKv|D^LIA0u zt)!q8KoVVj`^srsGN~Q1)mtuGE`#kg$4UUmyg+$GBaW%)=q5eKW(&{7TD7l~a!gHB zj2s@G4Q(Zp!|J7&90-v;DwR`7yHd{B=$|lCw`V+K_B_0KQoB1C1pAmgNf-e4QJ!*J zR7y50F%#As00xO(LoF?#s56TsZm^P17p21nWxfona+5!#}p+fq55S+x` zbSk8=VmqGc;QI%2`Z+=&vl_BDEzo2x=JKS*F0%?Myy_Ay^1$ z0v$2(p-bgNSviHN0tiYT91XyV5e%lCplFvwIjJl+%E?#miib8$Ol(41i5k>WeoG<; znI24#@^;~#ogqdjVU+azo?@avQbD5+Fpn5BPYspfO{LTTIzk>7)9MH%ZQhq8oC#~;&t*uB^ec90o#ua8Ky6_&7S*+SSt2kd##yUQi2 zFQl~KKu@B12^;}Eoz3h6op+~mEPp1OFu26pQg{hkGC*|(cKnxEI2Mb<<6V(>Z>%!{ z4=-A6i*~`YlMy2XUU(02t5heufZFibpyLN7{^Q&10-cr7#oA?5bq3OdDkH zMrv>}7E=8x{lx91+WAYg4lUQz5?77P>Zt&nR>&dJ&arg3_I76qZQbyX9fv&{7aWyi z?0_GnEdgehUhsd z*>pBhb}^A+hkyj!u``BBW>=B;b1_ZOK%w1HIE3OQ<{0Ikv=}*I1DcyNZ)tR0aRHg- zEI@(t2GI!Sn!Q@fytqo~h%Ep}R;6a}q?Am+(jngrp2+Db?2==#N3J3pMm*RLW=yyQ z7&ZugVhfAopSfh?6)rDui^TUNj>_6I4qa|x$Y>!^-7M+Qc;X3?<`Ez*)K6c`v_>vy4tD;o-WvwKl}|x5KX8ylOS8ll~bwz(=l=a4r#TpKpibG6}-3)|-Bh7VOyI&x%WVtR78Ha^mZZH$?T z>cV*K2lX9ce%k0K{>b>vwSg`!wiqEMRhLOKZ- zxr`+u_&t-h5v{C9Z&Is$DL{@ZUiZawJBizrn;F`4n4CxOx>97k$dyhQ_29_X7U(8k zeVNJ;ZZY&~CQ-?ig?>@KH;X`H2cVnmze~v$5=q@0B^o&p4Q1%0jRW&KtGjyq*&T80 zMSCTks%^uNi&WLx#b$S(V^X{}%ns(VgA{||fX09;WU5>QjV$udOoSwJ`?*y;mZ?Ow zp@vmHqxRsLE42InV$hR+Nc}p*=^!mf5&%KPo5KQ%^A`KJSex;BO4s!0b6u=Vr#B2R zI0PCQIC5b?2Vb>EBKeRP9K`w(cCfz_|CSgeO^M=4$%XEh@b2S`Y=+VuQjT@9=jReq zHZhLmY1hHnT0&B&3Gd@e(y8Z*Do0>2cQGL{;(uG-M>| z#2t07eQZqh7qUYMJ3oYwYiP?Wwp=_t0dqA`4b21Vm}o8Ziq^o}WZdp#cT)S?Gkk}MUwh`e>i zma0B2LbuodQntUYVGvDENHHV8T^_t#AC+C|ha^PmFT{%>DfCnUKC}R^p5rBs+yd?q?Ugy~p0^*I9Uh&i9vP$a zBvYf+*SU_Tv|lnkd*R&7@JQ9hLM9w(ADXF74JA`{xjI*CPgM`jQ{=&67-#9H_KPWo zE6YjyXij8<&8nK2!)fHGYSoN4P^1N3BEZTk35sJ-s1qB*LxVZm#U@3loEOlna$IJW2E}mCI1- zB~?o_-pW*@(4(`3D-({p>|!k!?+}0Ci%V)k4vQe_N1;@SeC7zX=j_Wj=EkVBrXwI@I-+ddMD%|Y9@CqMm>~*Qte)QBuRPLYT=tmLcBEav5EEh20KFWGo^#L+ahSEQ$7i>!E8k1=@u}%%9O~ zV8kT~1XN!Jpf`y9>NYg7x)pYDfHuYQrGy!1BnJ zyC52Dk_3q+sbA$f1!|CY4DckbRKZ8ro+!KNF3*V#Cz-$c?6fh|6C;<2yg8gx8G;K_ z!QvW+RiqC~vQ93C3iX$y&!OTfh69aAU?(}Sz$dy_zF~daQd&0ciD+X;6DK2L?DQT#q9XtZo$un#Y{=F!iMk+9KCLyWP?i zub(Axf|aJxRP3aENWFVIghL*1w^M(?rljIngDYs~fH;Hn6>pen24@pxDvMI0EG-5f zanj^7w-{6$iaPR~%TnBfT%fP%o zv?Sy5G`U4=;9eMO5`oKHZkTIBPTGhP_L_4E3e0$RoS_4ypfDz)*h`PbdVAtsaqRZs zq$xHy;+^Dj)m7+=PsMPW_}l>uv9SsR#B};eYGspB>@s7k7&1Yst*%xm#k5!SFzDn$ zyV)75t|JvahHr>~4`$_ybFGbUfJhgMys0qpW?8ByAnhociqiDG}L zzKU?et^+>q8OTUeAysg;-a{SOW21q*jxbRD(prqUp2QWkU2MQwaB-+csvUDaB=;op zY>N!6^7;Y!6`W{O{LD5)?)PeT<%o^sRF9CzZJc}}Il!?U6}4hPP%0m=I15@mFfVx| zkfz8;Nm0H%8L-96{zW{ncn<;nl2dCi7$;2*Gkc*BveY1~^-#??Jjn#pR)?@aTa@ez z;*`b${&;^`$07^_s>Lx&oIbYYn9n7gX1VpiXi=*&mqcoEx>Pu+{S+dpi_a5r7+IMn z$7pyLh8WE8^4&`VdF)K8WXIvG2z?rj7E}72JUB^AfT&hkU{r>eqzq;C{d5M!&9Qu( z5gS4MI46Tc2Ab(2j*~4Br+Q|g)u=@Ga%8u0HkVhVo-0tQia2W1L9qn&Nl11$gy3bY zlWNeTEa`GPHT86jniTdz&j=mLSg>}i4JKj4z&oH70G;~b4MGFdPQAUu{An_6DVtBJ zqYp+H#xuGy%=MtDjIl$$FtG;b-)J@tH#p`?t@&6(YWZd7Tk1!LNbGi-G*W&zVblaY zkuD*Jlo>SXVr5p>FOAQX;PikzKSM+5ce#cD_>QJA)e3u)q+4i$#Fd6Wrjv^7prhH+ zL4wpwj)&&eONPbLYNx@G2JJR0x)g>04s5n`nDn-;lsM-M4{wx{7qpv>Gw`8q0#z*p zSj0jRqc)xNA$PjII41rFd-EASO+T_H8X0cM$(Dw2cvXua#jMa=FWHqRMW>LkVCN+T zo=U{sE>b=2waK$7M$sl38$4L&e7QHSR$ZcI~jTq}7p(r$-_m^V$4Vd>@@fhq0vx#FfYH`Y;7Cuj|0WGQ{=@G3yma^s5p zw(58*xk`E;oXjGh*f-$Noa8>g-4IoBvBis*fgv1El!t*G9cVIxGZ9C!-8qiXIm#3I z$2e9}C?@*z37mt3Q$WrE_G;>8h7T?MX9eS=f~x%7}N&p|La?C(R}5!+!;mv?kYTh z%!Qk8l4s)|x&o->#pyn}no)omC zD+b2x8+xr$hG=l_5*}#)M~xbI08(XWPYIhwW*A-`g?8&04k7S1m^+3*bRrL`t^?gU zioiJ4t~55U2mm!Pa3jFfj)7@-dE1;Q(YQYK^is@?&Y`1#ziEUy=_D3&9&QZeI5=t0Gb+M}aO$CXjfsQb!=9iFiJ%@UV3$3^#9)IOETxFjyd9XnIch{60dO%>qMWZloq&Htkq*ZV^hMKyFR{DQ(bK^jw+71l zhK6!3PYQCk_-Ahz2Iw_-n^%Bjj9thv0W2gcWjX{Q-FKjM2@Y9YLP9}^Cvh@2UTAHn zb%ME@FT+ePW^sDR+&DkBBoV;USRS=?90VzfJf?T?lq@D$=`&L^SoOK|C@-VVN!%j< zSG25$c!(c7O9;JPgkM?8Q=<+Sb*J;JB8b3x>EyhlBfL8{StCljfw{V|mr*{KJ1nzE zuvr)2Gf+}TP%pYC9qrIhxl%9|TrQ2OL%CAg`Q#=}ob!(pbA0^J0TS2tBa#L@3~3&70nlL zS;BYO>Xoa+M*2-RF8bN{`FJK)H@($LtX`W~)y!Yq^wz}}OvGnUmwpD0YHw+}(tnlj z@2vl}t@S#J@LTxCD8k4MFMPX#zDhwKq7dRfe0!9he)&B~LEkKuY5bNRS3Qki-MiBG zS-mTbZ@jqD_$UT0`t~V3)q`F`bn^>*4MbicdiVuCpMndYKMB#(FILdQx%{5*n)(I4 z=V4rafsd0ImtWxXKE~x&|A>&z@(X;R$hiCh9}O`szrY7SjLR?ZF%skQ3w-{>xcuS- zg;AN=B!2q>eJqH6?=N$F{O=Mi80ioSW{os`G30GV8sAb8R~hN;NWa@iXOX7Q1K@$n zhkq@}o!*1=^+x%As-`Z2uZnm2CNE`y&$+$k2kLJD`SfjCD)-?(>hBSirkuZ{0?^u`BO#f>P`X=+CBJ!Z8364LbjTRfcppEb(AiF|NfNXnk>*HHfPI|eVxT}ASPnr?Ux5Z(6pIFzR zJ{aBUkLs^9(sEpRC-^s_y~ZcazZGdce|*D8Y31Nl|IIM!?*>o#hdgQUf`jk1_youoJ>}o^q<`*7`=L_J`O&{)W~Mul*7RHp zdYmIwaELfyE%>?5Q~%YT^pq$4Mo;>kp7cjN>Hq0T)2EqD_#X45pYxyeB>8Nnh+qzYXa+zVN+RPx&X2J_`OG0RD~xKELcKf6$ZuxhMU5q@P#v zPVJxfl&^sa)b@(#b3D@g$!d&8^0XOgbH3f4^nfRQz>}Wwq_6O#-{VQ&;Yr``Nq-+{ z6W*VB%AfY6{V?e!{4XOD51yurj%NS#1zIzGlPCRA zPx>pK^h2KX~h%lYW~geUm4Brzd@%C;gBo{g@~HtS7w|E~Yu(&7O43liuM;@Assa z@U*AJ={1p?o}Nig&m2kNmrvQLahZ`BpP1NRot-{7H^&t+)3a54mr7)(@N2U1TGggj z;`C85GFv-X#dqDp(^!wC2k2;4K5h5m0}cq@B5QQoJ~T0XaCpKVtxeC)*~9Y-LjH8B zRvit~=0SrRHVtAA&(01XvGEPL*&||Xc6hRCkIqj{9udbrZlhuV33$ilLxSvTHHt20W~+z?wzx|#>9BQ^ zjhPwcY#y-yoH(o3DHX8AXcrgEJU)k zZ*dGT6M6b6Ki!R7dv2V5X(tNRsnODIr?0_$)_J#W{JL&*0SquIl8M2XUECWryCB4N zm>G=Nael*&Xo8ZjPUeTFAXJVHA`>Ev$+>XA9OIr$X*WLop<;Oi zots!>)r2uRepz7-zh_UX-%JB}i{Q>IRs*OO#peO*YR|qZ1v60xpf=Ej$p2rWM?879@lFZ!a zaIH2wesI24MU7GVffF?2*tC8hK6nt`os*g!o;p;oI6HM{VtlUVG{o0)XPuT4^c`K* zBW5SUaP21HvFSYa&vb$+1E^VY>i@zEUpI!g?kwF}uvnwGuXE|wvs67SlQ=GXA3~%Q zL(&(PzkE@glCrV<=k@og?ocV0QAK>W*?FgR`cn1I`wQ!(Q4gTM=(q%98GjTFrll&4 zy8-ZN*pbEWlsN7U?QZei^ihsZ}A5l&u{`a8W$g|b}n7-2IS~>D$JqY6Hcvq*=?SBJqT&v)S%v- zo_gO$y#b>h!A{qg@n?=3LP(tUJ@lo^zKsn1PGvdNyWI%S>#`qv>OFNj_)jCvDEpOq z*SP3w72-?^j;o9MqrYd=Q`b8@CPY%zp@**bf_m3QA49#pI)SGKVI>++8{J>iAt5d_ z>goRINj#;m8PqFaJA>lbefXpP*5VJ#OU)~I6XJ_tpjpaP7xhCI?NpGg=__^_ctIB; HQ@{Qf0)0XW literal 0 HcmV?d00001