Micooz

Make something different!


  • Home

  • About

  • Tags

  • Archives

  • Search

CMake官方Tutorial中英对照翻译

Posted on 2015-01-08 |

CMake Tutorial

原版英文Tutorial:

http://www.cmake.org/cmake-tutorial/

Step1-Step7的源代码及CMakeLists:

http://public.kitware.com/cgi-bin/viewcvs.cgi/CMake/Tests/Tutorial/

Below is a step-by-step tutorial covering common build system issues
that CMake helps to address. Many of these topics have been introduced
in Mastering CMake as separate issues but seeing how they all work
together in an example project can be very helpful. This tutorial can
be found in the Tests/Tutorial directory of the CMake source code
tree. Each step has its own subdirectory containing a complete copy of
the tutorial for that step

下面是涵盖CMake解决常见构建系统问题的一个手把手教程。通过单独的例子,阐述了CMake是如何协同工作的,这将会对你十分有帮助。这个教程可以在CMake源代码树的Test/Tutorial目录下找到。每个步骤有各自的子目录,且包含了一份关于这个步骤的完整教程。

A Basic Starting Point (Step1) 简单的开始

The most basic project is an executable built from source code files.
For simple projects a two line CMakeLists file is all that is
required. This will be the starting point for our tutorial. The
CMakeLists file looks like:

最基本的工程是从源代码文件构建出可执行程序。对于简单的工程而言,一个两行的CMakeLists文件已经足够了。这将会是我们教程开始的第一步。CMakeLists文件看起来像这样:

1
2
3
4
5
6

cmake_minimum_required (VERSION 2.6)

project (Tutorial)

add_executable(Tutorial tutorial.cxx)

Note that this example uses lower case commands in the CMakeLists
file. Upper, lower, and mixed case commands are supported by CMake.
The source code for tutorial.cxx will compute the square root of a
number and the first version of it is very simple, as follows:

注意到这个例子在CMakeLists中用的是小写命令。无论是大写、小写、还是混合大小写,CMake都支持。
源代码文件tutorial.cxx将计算一个数的平方根,第一个版本十分简单:

<!-- lang: cpp -->
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main (int argc, char *argv[])
{
  if (argc < 2)
    {
    fprintf(stdout,"Usage: %s number\n",argv[0]);
    return 1;
    }
  double inputValue = atof(argv[1]);
  double outputValue = sqrt(inputValue);
  fprintf(stdout,"The square root of %g is %g\n",
          inputValue, outputValue);
  return 0;
}

Adding a Version Number and Configured Header File 添加一个版本号和用于配置的头文件

The first feature we will add is to provide our executable and project
with a version number. While you can do this exclusively in the source
code, doing it in the CMakeLists file provides more flexibility. To
add a version number we modify the CMakeLists file as follows:

我们将为可执行程序和工程提供一个版本号作为第一个特性。当然你可以在源代码中专门这样做,但是使用CMakeLists文件将更加灵活。为了添加一个版本号,我们修改CMakeLists文件如下:

<!-- lang: shell -->
cmake_minimum_required (VERSION 2.6)
project (Tutorial)
# The version number.
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)

# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
  "${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
  "${PROJECT_BINARY_DIR}/TutorialConfig.h"
  )

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories("${PROJECT_BINARY_DIR}")

# add the executable
add_executable(Tutorial tutorial.cxx)

Since the configured file will be written into the binary tree we must
add that directory to the list of paths to search for include files.
We then create a TutorialConfig.h.in file in the source tree with the
following contents:

由于配置文件将被写入二进制树,我们必须要在路径列表中添加目录以搜索包含文件。
然后我们在源代码树中添加TutorialConfig.h.in文件:

<!-- lang: cpp -->
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@

When CMake configures this header file the values for
@Tutorial_VERSION_MAJOR@ and @Tutorial_VERSION_MINOR@ will be replaced
by the values from the CMakeLists file. Next we modify tutorial.cxx to
include the configured header file and to make use of the version
numbers. The resulting source code is listed below.

CMake在配置过程中,将从CMakeLists文件中找到并替换头文件中的@Tutorial_VERSION_MAJOR@和@Tutorial_VERSION_MINOR@。之后我们修改tutorial.cxx,使它包含配置头文件,然后利用版本号。最终的源代码如下:

<!-- lang: cpp -->
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"

int main (int argc, char *argv[])
{
  if (argc < 2)
    {
    fprintf(stdout,"%s Version %d.%d\n",
            argv[0],
            Tutorial_VERSION_MAJOR,
            Tutorial_VERSION_MINOR);
    fprintf(stdout,"Usage: %s number\n",argv[0]);
    return 1;
    }
  double inputValue = atof(argv[1]);
  double outputValue = sqrt(inputValue);
  fprintf(stdout,"The square root of %g is %g\n",
          inputValue, outputValue);
  return 0;
}

The main changes are the inclusion of the TutorialConfig.h header file
and printing out a version number as part of the usage message.

主要的改变是添加了TutorialConfig.h头文件以及在使用帮助中打印出了版本号。

Adding a Library (Step 2) 添加一个库

Now we will add a library to our project. This library will contain
our own implementation for computing the square root of a number. The
executable can then use this library instead of the standard square
root function provided by the compiler. For this tutorial we will put
the library into a subdirectory called MathFunctions. It will have the
following one line CMakeLists file:

现在我们将为工程添加一个库。这个库将包含计算一个数的平方根的实现代码。可执行程序可以使用这个库,来替代编译器提供的标准平方根函数。在这个教程中,我们将把库放进一个叫做MathFunctions的子目录中。CMakeLists文件将包含这样一行:

<!-- lang: shell -->
add_library(MathFunctions mysqrt.cxx)

The source file mysqrt.cxx has one function called mysqrt that
provides similar functionality to the compiler’s sqrt function. To
make use of the new library we add an add_subdirectory call in the top
level CMakeLists file so that the library will get built. We also add
another include directory so that the MathFunctions/mysqrt.h header
file can be found for the function prototype. The last change is to
add the new library to the executable. The last few lines of the top
level CMakeLists file now look like:

源代码文件mysqrt.cxx有一个叫做mysqrt的函数,提供了一个类似于编译器sqrt函数的功能。为了能利用这个新库,我们在CMakeLists文件的顶部添加一个add_subdirectory调用来使之能够被构建。同时,我们也添加一个包含目录使MathFunctions/mysqrt.h头文件提供函数原型。最后一个改变是给可执行程序添加新库。CMakeLists文件的最后几行看起来像这样:

<!-- lang: shell -->
include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
add_subdirectory (MathFunctions) 

# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial MathFunctions)

Now let us consider making the MathFunctions library optional. In this
tutorial there really isn’t any reason to do so, but with larger
libraries or libraries that rely on third party code you might want
to. The first step is to add an option to the top level CMakeLists
file.

现在让我们考虑使MathFunctions库可选。这个教程中确实没有任何理由这样做,但是对于更大的库或依赖于第三方代码的库来说,你可能更愿意这样做。第一步是在CMakeLists中添加一个选项:

<!-- lang: shell -->
# should we use our own math functions?
option (USE_MYMATH 
        "Use tutorial provided math implementation" ON) 

This will show up in the CMake GUI with a default value of ON that the
user can change as desired. This setting will be stored in the cache
so that the user does not need to keep setting it each time they run
CMake on this project. The next change is to make the build and
linking of the MathFunctions library conditional. To do this we change
the end of the top level CMakeLists file to look like the following:

这将在CMake GUI中默认显示为ON,这样使用者可以根据需要来改变它。这个配置将被保存在缓存中,这样使用者不必在工程中使用CMake时每次都去配置它。下一个改变是使构建和链接MathFunctions库有条件化。为了达到目的我们改变CMakeLists文件如下:

<!-- lang: shell -->
# add the MathFunctions library?
#
if (USE_MYMATH)
  include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
  add_subdirectory (MathFunctions)
  set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)

# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial  ${EXTRA_LIBS})

This uses the setting of USE_MYMATH to determine if the MathFunctions
should be compiled and used. Note the use of a variable (EXTRA_LIBS in
this case) to collect up any optional libraries to later be linked
into the executable. This is a common approach used to keep larger
projects with many optional components clean. The corresponding
changes to the source code are fairly straight forward and leave us
with:

它使用了USE_MYMATH设置来决定MathFunctions是否应该被编译和使用。注意变量EXTRA_LIBS(本例中)用来收集接下来将被链接到可执行程序中的可选库。这是一个保持大型工程和许多可选组件干净的常用方法。在源代码中的相应改变非常简单:

<!-- lang: cpp -->
// A simple program that computes the square root of a number
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "TutorialConfig.h"
#ifdef USE_MYMATH
#include "MathFunctions.h"
#endif

int main (int argc, char *argv[])
{
  if (argc < 2)
    {
    fprintf(stdout,"%s Version %d.%d\n", argv[0],
            Tutorial_VERSION_MAJOR,
            Tutorial_VERSION_MINOR);
    fprintf(stdout,"Usage: %s number\n",argv[0]);
    return 1;
    }

  double inputValue = atof(argv[1]);

#ifdef USE_MYMATH
  double outputValue = mysqrt(inputValue);
#else
  double outputValue = sqrt(inputValue);
#endif

  fprintf(stdout,"The square root of %g is %g\n",
          inputValue, outputValue);
  return 0;
}

In the source code we make use of USE_MYMATH as well. This is provided
from CMake to the source code through the TutorialConfig.h.in
configured file by adding the following line to it:

在源代码中我们利用了USE_MYMATH。它是由TutorialConfig.h.in文件通过CMake提供的:

<!-- lang: cpp -->
#cmakedefine USE_MYMATH

Installing and Testing (Step 3) 安装和测试

For the next step we will add install rules and testing support to our
project. The install rules are fairly straight forward. For the
MathFunctions library we setup the library and the header file to be
installed by adding the following two lines to MathFunctions’
CMakeLists file:

下一步我们将给工程添加安装规则和测试支持。安装规则相当简单。

对于MathFunctions库,我们通过在MathFunction的CMakeLists文件中添加下面两行,来使它的库和头文件可以被安装:

<!-- lang: shell -->
install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)

For the application the following lines are added to the top level
CMakeLists file to install the executable and the configured header
file:

对于应用程序,在CMakeLists中添加下面的命令来安装可执行程序和配置文件:

<!-- lang: shell -->
# add the install targets
install (TARGETS Tutorial DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"        
         DESTINATION include)

That is all there is to it. At this point you should be able to build
the tutorial, then type make install (or build the INSTALL target from
an IDE) and it will install the appropriate header files, libraries,
and executables. The CMake variable CMAKE_INSTALL_PREFIX is used to
determine the root of where the files will be installed. Adding
testing is also a fairly straight forward process. At the end of the
top level CMakeLists file we can add a number of basic tests to verify
that the application is working correctly.

就是这么一回事,现在你能够构建这个教程了,然后执行make install(或者通过IDE构建INSTALL目标),它将安装适当的头文件,库和可执行程序。CMake变量CMAKE_INSTALL_PREFIX用来决定所安装文件的根目录。

添加测试也非常简单。在顶级CMakeLists文件的最后,我们可以添加一系列基础测试来验证应用程序是否运行正常。

<!-- lang: shell -->
# does the application run
add_test (TutorialRuns Tutorial 25)

# does it sqrt of 25
add_test (TutorialComp25 Tutorial 25)

set_tests_properties (TutorialComp25 
  PROPERTIES PASS_REGULAR_EXPRESSION "25 is 5")

# does it handle negative numbers
add_test (TutorialNegative Tutorial -25)
set_tests_properties (TutorialNegative
  PROPERTIES PASS_REGULAR_EXPRESSION "-25 is 0")

# does it handle small numbers
add_test (TutorialSmall Tutorial 0.0001)
set_tests_properties (TutorialSmall
  PROPERTIES PASS_REGULAR_EXPRESSION "0.0001 is 0.01")

# does the usage message work?
add_test (TutorialUsage Tutorial)
set_tests_properties (TutorialUsage
  PROPERTIES 
  PASS_REGULAR_EXPRESSION "Usage:.*number")

The first test simply verifies that the application runs, does not
segfault or otherwise crash, and has a zero return value. This is the
basic form of a CTest test. The next few tests all make use of the
PASS_REGULAR_EXPRESSION test property to verify that the output of the
test contains certain strings. In this case verifying that the
computed square root is what it should be and that the usage message
is printed when an incorrect number of arguments are provided. If you
wanted to add a lot of tests to test different input values you might
consider creating a macro like the following:

第一个测试例子验证了程序运行,是否出现段错误或者崩溃,是否返回0值。这是一个CTest的基本测试。接下来的几个测试都利用了PASS_REGULAR_EXPRESSION来验证输出是否包含指定的字符串。在这种情况下,验证计算平方根是必要的,当提供了一个错误的参数时就打印用法信息。如果你想添加测试多个不同的输入值,可以考虑写一个像下面这样的宏:

<!-- lang: shell -->
#define a macro to simplify adding tests, then use it
macro (do_test arg result)
  add_test (TutorialComp${arg} Tutorial ${arg})
  set_tests_properties (TutorialComp${arg}
    PROPERTIES PASS_REGULAR_EXPRESSION ${result})
endmacro (do_test)

# do a bunch of result based tests
do_test (25 "25 is 5")
do_test (-25 "-25 is 0")

For each invocation of do_test, another test is added to the project
with a name, input, and results based on the passed arguments.

对于每个do_test调用,都会根据传入其中的参数,在工程中添加名称、输入和结果。

Adding System Introspection (Step 4) 添加系统反馈

Next let us consider adding some code to our project that depends on
features the target platform may not have. For this example we will
add some code that depends on whether or not the target platform has
the log and exp functions. Of course almost every platform has these
functions but for this tutorial assume that they are less common. If
the platform has log then we will use that to compute the square root
in the mysqrt function. We first test for the availability of these
functions using the CheckFunctionExists.cmake macro in the top level
CMakeLists file as follows:

接下来让我们考虑在工程中添加一些代码,依赖目标平台上可能没有的特性。在这个例子中,我们将添加一些依赖于目标平台是否存在log和exp函数的代码。当然几乎所有的平台都有这些函数,但这个教程假设它们(这些函数)不常见。如果平台存在log,我们就在mysqrt中使用它来计算平方根。我们首先在顶级CMakeLists中使用CheckFunctionExists.cmake宏来测试这些函数的可用性:

<!-- lang: shell -->
# does this system provide the log and exp functions?
include (CheckFunctionExists.cmake)
check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)

Next we modify the TutorialConfig.h.in to define those values if CMake
found them on the platform as follows:

接下来我们修改TutorialConfig.h.in,如果CMake在平台上找到他们(函数),就定义这些值:

<!-- lang: shell -->
// does the platform provide exp and log functions?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP

It is important that the tests for log and exp are done before the
configure_file command for TutorialConfig.h. The configure_file
command immediately configures the file using the current settings in
CMake. Finally in the mysqrt function we can provide an alternate
implementation based on log and exp if they are available on the
system using the following code:

在为TutorialConfig.h执行configure_file命令之前完成对log和exp的测试非常重要。configure_file命令会使用CMake的当前设置立即配置这个文件。最后我们可以在mysqrt函数中提供基于log和exp的可选实现(如果他们在当前系统上可用):

<!-- lang: cpp -->
// if we have both log and exp then use them
#if defined (HAVE_LOG) && defined (HAVE_EXP)
  result = exp(log(x)*0.5);
#else // otherwise use an iterative approach
  . . .

Adding a Generated File and Generator (Step 5) 添加生成的文件和生成器

In this section we will show how you can add a generated source file
into the build process of an application. For this example we will
create a table of precomputed square roots as part of the build
process, and then compile that table into our application. To
accomplish this we first need a program that will generate the table.
In the MathFunctions subdirectory a new source file named
MakeTable.cxx will do just that.

在这个单元中我们将为你展示如何在应用程序的构建过程中添加生成好的源文件。这个例子中我们将为构建过程造一个预计算平方根的表,然后将这个表编译进我们的应用程序。为了完成这个,我们首先需要一个可以生成这个表的程序。MathFunctions子目录下的MakeTable.cxx新源文件将完成这个工作:

<!-- lang: cpp -->
// A simple program that builds a sqrt table 
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main (int argc, char *argv[])
{
  int i;
  double result;

  // make sure we have enough arguments
  if (argc < 2)
    {
    return 1;
    }

  // open the output file
  FILE *fout = fopen(argv[1],"w");
  if (!fout)
    {
    return 1;
    }

  // create a source file with a table of square roots
  fprintf(fout,"double sqrtTable[] = {\n");
  for (i = 0; i < 10; ++i)
    {
    result = sqrt(static_cast<double>(i));
    fprintf(fout,"%g,\n",result);
    }

  // close the table with a zero
  fprintf(fout,"0};\n");
  fclose(fout);
  return 0;
}

Note that the table is produced as valid C++ code and that the name of
the file to write the output to is passed in as an argument. The next
step is to add the appropriate commands to MathFunctions’ CMakeLists
file to build the MakeTable executable, and then run it as part of the
build process. A few commands are needed to accomplish this, as shown
below.

注意这个表格由C++代码产生,文件名作为参数传入以在里面输出结果。下一步是给MathFunctions的CMakeLists添加适当的命令来构建MakeTable程序。之后将作为构建过程的一部分来运行。需要很少的命令来完成这件事:

<!-- lang: shell -->
# first we add the executable that generates the table
add_executable(MakeTable MakeTable.cxx)

# add the command to generate the source code
add_custom_command (
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  DEPENDS MakeTable
  )

# add the binary tree directory to the search path for 
# include files
include_directories( ${CMAKE_CURRENT_BINARY_DIR} )

# add the main library
add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h  )

First the executable for MakeTable is added as any other executable
would be added. Then we add a custom command that specifies how to
produce Table.h by running MakeTable. Next we have to let CMake know
that mysqrt.cxx depends on the generated file Table.h. This is done by
adding the generated Table.h to the list of sources for the library
MathFunctions. We also have to add the current binary directory to the
list of include directories so that Table.h can be found and included
by mysqrt.cxx. When this project is built it will first build the
MakeTable executable. It will then run MakeTable to produce Table.h.
Finally, it will compile mysqrt.cxx which includes Table.h to produce
the MathFunctions library. At this point the top level CMakeLists file
with all the features we have added looks like the following:

首先,MakeTable像其他任何一个可执行程序一样被添加进入。然后,我们添加一个定制的命令来指定如何运行MakeTable来产生Table.h。之后,我们必须让CMake知道mysqrt.cxx依赖生成的Table.h文件。将生成的Table.h添加到MathFunctions源代码列表中来搞定它。同时我们也必须将当前二进制目录添加到包含目录列表中,使得Table.h可以被找到,以及被mysqrt.cxx包含。

但工程开始构建时,他首先会构建MakeTable程序,然后执行MakeTable来产生Table.h。最后,他会编译包含了Table.h的mysqrt.cxx来生成MathFunctions库。

至此,顶层的CMakeLists文件包含了我们之前添加的所有特性:

<!-- lang: shell -->
cmake_minimum_required (VERSION 2.6)
project (Tutorial)

# The version number.
set (Tutorial_VERSION_MAJOR 1)
set (Tutorial_VERSION_MINOR 0)

# does this system provide the log and exp functions?
include (${CMAKE_ROOT}/Modules/CheckFunctionExists.cmake)

check_function_exists (log HAVE_LOG)
check_function_exists (exp HAVE_EXP)

# should we use our own math functions
option(USE_MYMATH 
  "Use tutorial provided math implementation" ON)

# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
  "${PROJECT_SOURCE_DIR}/TutorialConfig.h.in"
  "${PROJECT_BINARY_DIR}/TutorialConfig.h"
  )

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories ("${PROJECT_BINARY_DIR}")

# add the MathFunctions library?
if (USE_MYMATH)
  include_directories ("${PROJECT_SOURCE_DIR}/MathFunctions")
  add_subdirectory (MathFunctions)
  set (EXTRA_LIBS ${EXTRA_LIBS} MathFunctions)
endif (USE_MYMATH)

# add the executable
add_executable (Tutorial tutorial.cxx)
target_link_libraries (Tutorial  ${EXTRA_LIBS})

# add the install targets
install (TARGETS Tutorial DESTINATION bin)
install (FILES "${PROJECT_BINARY_DIR}/TutorialConfig.h"        
         DESTINATION include)

# does the application run
add_test (TutorialRuns Tutorial 25)

# does the usage message work?
add_test (TutorialUsage Tutorial)
set_tests_properties (TutorialUsage
  PROPERTIES 
  PASS_REGULAR_EXPRESSION "Usage:.*number"
  )


#define a macro to simplify adding tests
macro (do_test arg result)
  add_test (TutorialComp${arg} Tutorial ${arg})
  set_tests_properties (TutorialComp${arg}
    PROPERTIES PASS_REGULAR_EXPRESSION ${result}
    )
endmacro (do_test)

# do a bunch of result based tests
do_test (4 "4 is 2")
do_test (9 "9 is 3")
do_test (5 "5 is 2.236")
do_test (7 "7 is 2.645")
do_test (25 "25 is 5")
do_test (-25 "-25 is 0")
do_test (0.0001 "0.0001 is 0.01")

TutorialConfig.h looks like:

TutorialConfig.h如下:

<!-- lang: cpp -->
// the configured options and settings for Tutorial
#define Tutorial_VERSION_MAJOR @Tutorial_VERSION_MAJOR@
#define Tutorial_VERSION_MINOR @Tutorial_VERSION_MINOR@
#cmakedefine USE_MYMATH

// does the platform provide exp and log functions?
#cmakedefine HAVE_LOG
#cmakedefine HAVE_EXP

And the CMakeLists file for MathFunctions looks like:

MathFunctions的CMakeLists如下:

<!-- lang: shell -->
# first we add the executable that generates the table
add_executable(MakeTable MakeTable.cxx)
# add the command to generate the source code
add_custom_command (
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  DEPENDS MakeTable
  COMMAND MakeTable ${CMAKE_CURRENT_BINARY_DIR}/Table.h
  )
# add the binary tree directory to the search path 
# for include files
include_directories( ${CMAKE_CURRENT_BINARY_DIR} )

# add the main library
add_library(MathFunctions mysqrt.cxx ${CMAKE_CURRENT_BINARY_DIR}/Table.h)

install (TARGETS MathFunctions DESTINATION bin)
install (FILES MathFunctions.h DESTINATION include)

Building an Installer (Step 6) 构建一个安装程序

Next suppose that we want to distribute our project to other people so
that they can use it. We want to provide both binary and source
distributions on a variety of platforms. This is a little different
from the instal we did previously in section Installing and Testing
(Step 3), where we were installing the binaries that we had built from
the source code. In this example we will be building installation
packages that support binary installations and package management
features as found in cygwin, debian, RPMs etc. To accomplish this we
will use CPack to create platform specific installers as described in
Chapter Packaging with CPack. Specifically we need to add a few lines
to the bottom of our toplevel CMakeLists.txt file.

下面假设我们想发布我们的工程给其他人使用。我们想同时在多个平台上提供二进制程序和源代码。这和我们之前在第三部分Installing and Testing中做的安装(从源代码构建安装)有一点不同。这个例子中,我们将构建一个支持二进制安装和包管理特性的安装包,就像在cygwin、debian、RPM中找到的安装包一样。为了完成这件事,我们将利用在章节Packaging with CPack提到的CPack制作指定平台的安装程序。特别地,我们需要在顶层CMakeLists下面添加几行:

<!-- lang: shell -->
# build a CPack driven installer package
include (InstallRequiredSystemLibraries)
set (CPACK_RESOURCE_FILE_LICENSE  
     "${CMAKE_CURRENT_SOURCE_DIR}/License.txt")
set (CPACK_PACKAGE_VERSION_MAJOR "${Tutorial_VERSION_MAJOR}")
set (CPACK_PACKAGE_VERSION_MINOR "${Tutorial_VERSION_MINOR}")
include (CPack)

That is all there is to it. We start by including
InstallRequiredSystemLibraries. This module will include any runtime
libraries that are needed by the project for the current platform.
Next we set some CPack variables to where we have stored the license
and version information for this project. The version information
makes use of the variables we set earlier in this tutorial. Finally we
include the CPack module which will use these variables and some other
properties of the system you are on to setup an installer. The next
step is to build the project in the usual manner and then run CPack on
it. To build a binary distribution you would run:

就是这样,先包含InstallRequiredSystemLibraries。这个模块将包含工程在当前平台需要的任何运行库。然后,我们设置一些CPack变量指向工程存放license和version信息的地方。版本信息利用了我们之前在教程里设置的变量。最后,我们包含的CPack模块将会使用这些变量,以及你所在系统的其他一些属性,来制作一个安装包。

下一步是按照平常的方式构建工程然后启动CPack。要发布一个二进制版,你需要执行:

<!-- lang: shell -->
cpack -C CPackConfig.cmake

To create a source distribution you would type

要发布源代码,你需要执行:

<!-- lang: shell -->
cpack -C CPackSourceConfig.cmake

Adding Support for a Dashboard (Step 7) 添加面板支持

Adding support for submitting our test results to a dashboard is very
easy. We already defined a number of tests for our project in the
earlier steps of this tutorial. We just have to run those tests and
submit them to a dashboard. To include support for dashboards we
include the CTest module in our toplevel CMakeLists file.

为提交我们的测试结果添加面板支持十分简单。在之前的教程中,我们已经为工程定义了很多测试。我们仅仅需要执行这些测试以及把它们提交到面板。要包含面板支持我们需要在顶层CMakeLists中包含CTest模块。

<!-- lang: shell -->
# enable dashboard scripting
include (CTest)

We also create a CTestConfig.cmake file where we can specify the name
of this project for the dashboard.

我们还为面板写了一个可以指定工程名的CTestConfig.cmake文件。

<!-- lang: shell -->
set (CTEST_PROJECT_NAME "Tutorial")

CTest will read in this file when it runs. To create a simple
dashboard you can run CMake on your project, change directory to the
binary tree, and then run ctest –D Experimental. The results of your
dashboard will be uploaded to Kitware’s public dashboard here.

CTest将在运行时读取这个文件。要创建一个简单的面板,你可以在你的工程里运行CMake,将目录改变到二进制树,然后执行实验性的ctest -D。你面板上的结果将会被上传到Kitware的公开面板。

结束语

由于本人水平所限,译文和原文内容难免存在差异,甚至错误,还请各位读者评批指出!谢谢!

C++ 字符编码问题探究和中文乱码的产生

Posted on 2015-01-02 |

引言

一直以来,C/C++对中文字符的处理时常让人摸不着头脑。

主要有下面几个原因:

  1. 文件编码方式的差异

  2. 系统环境对中文的解释有差异

  3. 不同编译器对标准库的实现有差异

而这三者往往又相互影响,暗藏玄机,让人抓狂。

在写本文之前我查阅了很多博客,关于中文的输入输出,cout,wcout,fstream,wfstream,乱码解决方案等等问题都有了十分详细的解答,但是,很多博文具有片面性。

许多博主仅仅是针对自己所使用的环境做阐述,而又没有明确指明使用了何种IDE,何种编译器,何种系统。结果就是,博主们高高兴兴的解决的自己的问题并分享出来,大家高高兴兴的点赞,觉得自己和博主的问题是同一个问题,实际情况却大相径庭。

必要的说明

文本涉及的编译器和系统:

  • msvc v120 Windows 8.1

  • mingw 4.8 32bit Windows 8.1

  • g++ 4.8.2 Linux 64bit

开始测试

测试之前很有必要说明一点:

A program should not mix output operations on wcout with output operations on cout (or with other narrow-oriented output operations on stdout): Once an output operation has been performed on either, the standard output stream acquires an orientation (either narrow or wide) that can only be safely changed by calling freopen on stdout.
—- cplusplus.com

就是说不要混用cout和wcout进行输出,因此下面的例子中都是单独使用cout或者wcout。

cout测试

下面的测试在Visual Studio 2013中进行。

MSVC,默认编码GB2312(可以在 文件–高级保存选项 中查看和修改)

<!-- lang: cpp -->
#include <iostream>

int main() {
    using namespace std;

    const char *code = "abc中文def";

    cout << "abc中文def" << endl;
    cout << code << endl;

    return 0;
}

结果

abc中文def

abc中文def

均正确输出。

MSVC,改变编码为UTF8(+bom)

结果

abc中文def

abc中文def

均正确输出。

MSVC,改变编码为UTF8(-bom)

结果

abc涓枃def

abc涓枃def

出现乱码。

问题分析

可以看到源文件的编码方式会影响最后的输出,原因在于常量文本采用了硬编码的方式,也就是说源代码里面的中文会根据当前文件的编码方式直接翻译成对应字节码放进存储空间。

如“中文”二字,

GB2312(Codepage 936)的编码为:

D6 D0 CE C4

而UTF8是:

E4 B8 AD E6 96 87

而控制台也有一套编码方式,对于Windows的cmd,可以查看其 属性 下面的当前代码页,笔者是ANSI(936)。

当向控制台传送GB2312的字节码时,中文显示正常,当传入无签名的UTF8的字节码时,中文就不能被正确解释,出现了乱码。

Q:为什么带有签名的UTF8却可以正常显示呢?

A:实际上UTF8完全不需要带签名,M$自作聪明YY了一个bom头来识别文件是不是UTF8。因此带有签名的UTF8能被cmd识别为UTF8,中文才能显示正常。

为了进一步证实是不是和控制台的编码有关系,并正确理解上一个例子中乱码的产生缘由,我们可以做一个重定向,将结果输出到文本文件:

test.exe > test.txt

使用任意可以改变编码的文本编辑器(笔者使用的是everedit)查看,可以发现以UTF8解释,显示正常,以ANSI(936)解释,将得到刚才那个乱码。


下面的测试在QtCreator中进行。

MinGW,UTF8

结果

abc涓枃def

abc涓枃def

出现乱码。

MinGW,ANSI-936

结果

abc中文def

abc中文def

显示正确。


下面的测试在Linux的bash中进行。

g++,UTF8

结果

abc中文def

abc中文def

显示正确。

g++,gb2312

结果

abc▒▒▒▒def

abc▒▒▒▒def

出现乱码。

Ubuntu查看/etc/default/locale,可以看到LANG=”en_US.UTF-8”,说明bash能解释UTF8的字节码,而gb2312的变成了乱码。

小结

程序的输出编码必须和”显示程序”的显示编码适配时才能得到正确的结果。简而言之就是解铃还须系铃人。


宽字符使用多个字节来表示一个字符,中文可以用char来表示没问题,用wchar来表示也没有问题。

wcout测试

wcout输出wchar_t型的宽字符,测试代码如下:

<!-- lang: cpp -->
#include <iostream>

int main() {
    using namespace std;

    const wchar_t *code = L"abc中文def";

    wcout << L"abc中文def" << endl;
    wcout << code << endl;

    return 0;
}

MSVC,无论上述何种编码

结果

abc

输出被截断,只有前几个英文字母可以被输出,传入指针输出无效。

问题分析

L”abc中文def” 在内存中表现为:

(gb2312) 61 00 62 00 63 00 2d 4e 87 65 64 00 65 00 66 00

(utf8-bom) 61 00 62 00 63 00 2d 4e 87 65 64 00 65 00 66 00

(utf8+bom)61 00 62 00 63 00 93 6d 5f e1 83 67 64 00 65 00 66 00

wcout 在处理L”abc中文def”时,按宽字节依次遍历,前面的abc没问题(小端序第一个字节是00),遇到中文,识别不了,无输出,间接导致后续<<都没有输出了。

也就是说wcout不能用来处理中文输出。

第二个传入wchar_t指针,发现没有任何输出,为了验证是不是由于上一条输出语句中中文的影响,单独测试如下:

<!-- lang: cpp -->
#include <iostream>

int main() {
    using namespace std;

    const wchar_t *code = L"abc中文def";

    wcout << code << endl;

    return 0;
}

结果

abc

说明传入wchar_t指针是可以正常输出宽字节英文的,一旦遇到非00字节间隔,后续所有输出将无效。

MinGW的结果同样如此,无论编码与否,只要wcout遇到中文立马跪。

有博主称可以在输出前执行下面的函数或者进行全局设置来让wcout认识中文:

<!-- lang: cpp -->
std::wcout.imbue(std::locale("chs"));
std::locale::global(std::locale(""));//全局设置
  • MSVC下,没有问题,可以达到预期结果。

  • MinGW下,第一条语句会抛出一个runtime_error异常崩溃掉,第二条语句无效。

  • Linux g++下,没问题。

可见MinGW的libstdc++对locale的实现不理想,有传闻使用stlport可以避免这个问题。


总结

  • 认清你的代码处在何种编码的环境

  • 认清放在你字符串里面的数据是何种编码

  • 认清你要向具有何种编码的屏幕传送数据

  • 解铃还须系铃人

  • 非特殊情况下不建议使用wchar_t来存放中文字符

很多时候中文并不是硬编码进程序的,例如一段中文来自网络,以gb2312编码,而”屏幕”只认UTF8,这个时候就要进行必要的编码转换。boost库的boost::locale::conv中提供了很多常用的转换模板函数,来实现宽窄字符、ansi和utf之间的相互转换。

C++使用Boost实现Network Time Protocol(NTP)客户端

Posted on 2014-10-01 |

引言

笔者机器上安装了两个系统,一个Linux Ubuntu,一个Windows8.1。让人感到郁闷的是,每次从Ubuntu重启进入Windows时,系统时间总是少了8个小时,每次都要用Windows的时间程序进行同步,也就是下面这个东西:

这个东西其实就是一个NTP Client,从Internet上选择一台NTP Server,获取UTC时间,然后设置本地时间。

于是我想自己实现一个这样的程序,先百度一下吧,网上有很多关于NTP的资料和实现代码,大多是单一平台的,不能跨平台

,下面给几个参考:

http://blog.csdn.net/loongee/article/details/24271129

http://blog.csdn.net/chexlong/article/details/6963541

http://www.cnblogs.com/TianFang/archive/2011/12/20/2294603.html

本文使用boost的Asio来跨平台实现NTP Client.

准备

  1. 最新的boost库,本文使用的是1.56.0版本
    要用到里面的ASIO网络库
  2. IDE是Visual Studio 2013 with Update3
    笔者是版本帝
  3. WireShark也是最新的1.12.1版本
    用来分析Windows自带的NTP Client

NTP Packet分析

这里我们分析的正是上图那个程序,点击立即更新,会发送NTP的请求包,下面是Wireshark的抓包结果:

可以得到下面一些信息:

  1. NTP时间同步分两个过程,一个Request,一个Response
  2. 这里的NTP Server的IP地址是129.6.15.28
  3. 程序没有进行DNS解析,可能是直接保存了IP地址
  4. NTP服务的端口号是123,Client也使用了123端口,后来发现Client不是一定要使用123端口的
  5. NTP协议是构建在UDP传输协议上的应用协议
  6. 这里使用V3版的NTP协议,目前还有v4

好了,有了关于NTP协议的一些基本信息,我们再来看看应用层的详细信息:

Response包:

分了很多字段,关于每个字段的含义请参考上面给出的链接,本文主要讲实现。这里Reference Timestamp就是Request包发送的Timestamp,而Origin,Receive,Transmit都是从Server返回回来的时间,后三个时间都相差非常小,因此方便一点,我们取最后一个Transmit Timestamp作为结果。

编码

boost里面相关库的编译可以参考官方的文档,里面有非常简单的例子。

1. 需要的头文件和名字空间
1
2
3
4
5
6
#include <iostream>
#include "boost/asio.hpp"
#include "boost/date_time/posix_time/posix_time.hpp"

using namespace boost::posix_time;
using namespace boost::asio::ip;
2. NtpPacket的构造
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class NtpPacket {
public:
NtpPacket() {
_rep._flags = 0xdb;
// 11.. .... Leap Indicator: unknown
// ..01 1... NTP Version 3
// .... .011 Mode: client
_rep._pcs = 0x00;//unspecified
_rep._ppt = 0x01;
_rep._pcp = 0x01;
_rep._rdy = 0x01000000;//big-endian
_rep._rdn = 0x01000000;
_rep._rid = 0x00000000;
_rep._ret = 0x0;
_rep._ort = 0x0;
_rep._rct = 0x0;
_rep._trt = 0x0;
}

friend std::ostream& operator<<(std::ostream& os, const NtpPacket& ntpacket) {
return os.write(reinterpret_cast<const char *>(&ntpacket._rep), sizeof(ntpacket._rep));
}

friend std::istream& operator>>(std::istream& is, NtpPacket& ntpacket) {
return is.read(reinterpret_cast<char*>(&ntpacket._rep), sizeof(ntpacket._rep));
}

public:
#pragma pack(1)
struct NtpHeader {
uint8_t _flags;//Flags
uint8_t _pcs;//Peer Clock Stratum
uint8_t _ppt;//Peer Polling Interval
uint8_t _pcp;//Peer Clock Precision
uint32_t _rdy;//Root Delay
uint32_t _rdn;//Root Dispersion
uint32_t _rid;//Reference ID
uint64_t _ret;//Reference Timestamp
uint64_t _ort;//Origin Timestamp
uint64_t _rct;//Receive Timestamp
uint64_t _trt;//Transmit Timestamp
};
#pragma pack()
NtpHeader _rep;
};

这里为了方便存取就没有把struct放到private中,需要注意的是结构体各个字段的顺序和需要进行内存1字节对齐,即使用:

1
#pragma pack(1)

内存对齐在网络编程中十分重要,他会直接影响Packet的内容,关于内存对齐可以参考:

http://www.cppblog.com/cc/archive/2006/08/01/10765.html

NTP请求包中最重要的是flags,里面存有版本信息等直接影响协议工作的内容,因此不能搞错了。

两个operator重载用来方便读写Packet数据。

再来看看Client类的实现,Client类的主要任务就是发送和接受NTP包,并返回最后那个64bit的Timestamp。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class NtpClient {
public:
NtpClient(const std::string& serverIp)
:_socket(io), _serverIp(serverIp) {
}

time_t getTime() {
if (_socket.is_open()) {
_socket.shutdown(udp::socket::shutdown_both, _ec);
if (_ec) {
std::cout << _ec.message() << std::endl;
_socket.close();
return 0;
}
_socket.close();
}
udp::endpoint ep(boost::asio::ip::address_v4::from_string(_serverIp), NTP_PORT);
NtpPacket request;
std::stringstream ss;
std::string buf;
ss << request;
ss >> buf;
_socket.open(udp::v4());
_socket.send_to(boost::asio::buffer(buf), ep);
std::array<uint8_t, 128> recv;
size_t len = _socket.receive_from(boost::asio::buffer(recv), ep);
uint8_t* pBytes = recv.data();
/****dump hex data
for (size_t i = 0; i < len; i++) {
if (i % 16 == 0) {
std::cout << std::endl;
}
else {
std::cout << std::setw(2) << std::setfill('0')
<< std::hex << (uint32_t) pBytes[i];
std::cout << ' ';
}
}
****/
time_t tt;
uint64_t last;
uint32_t seconds;
/****get the last 8 bytes(Transmit Timestamp) from received packet.
std::memcpy(&last, pBytes + len - 8, sizeof(last));
****create a NtpPacket*/
NtpPacket resonpse;
std::stringstream rss;
rss.write(reinterpret_cast<const char*>(pBytes), len);
rss >> resonpse;
last = resonpse._rep._trt;
//
reverseByteOrder(last);
seconds = (last & 0x7FFFFFFF00000000) >> 32;
tt = seconds + 8 * 3600 * 2 - 61533950;
return tt;
}

private:
const uint16_t NTP_PORT = 123;
udp::socket _socket;
std::string _serverIp;
boost::system::error_code _ec;
};

注意几个地方:

1. udp::socket是boost里面使用udp协议的套接字,他的构造需要一个io_service,io_service可以直接在全局区进行声明:
1
boost::asio::io_service io;
2. 创建一个endpoint用来表示NTP Server的地址:
1
udp::endpoint ep(boost::asio::ip::address_v4::from_string(_serverIp), NTP_PORT);

向这个ep send_to,并从这个ep receive_from数据包。

3. time_t的定义如下:
1
2
typedef __time64_t time_t;      /* time value */
typedef __int64 __time64_t; /* 64-bit time value */

也就是说这个time_t其实就是一个64bit的int,我们可以用uint64_t这个类型与之互换,他可以用来表示一个Timestamp。

  1. 获取最后8字节内容有两种方式,一种是直接复制pBytes的内存,一种是构造NtpPacket,然后取成员,这里选择后者易于理解。

  2. 字节序的问题

网络字节序都是大端模式,需要进行转换,由于仅仅需要最后那个uint64_t所以我写了一个针对64bit的字节序转换函数:

1
2
3
4
5
6
7
8
9
10
static void reverseByteOrder(uint64_t &in) {
uint64_t rs = 0;
int len = sizeof(uint64_t);
for (int i = 0; i < len; i++) {
std::memset(reinterpret_cast<uint8_t*>(&rs) + len - 1 - i
, static_cast<uint8_t> ((in & 0xFFLL << (i * 8)) >> i * 8)
, 1);
}
in = rs;
}

最后一个64bit内容的高32位存了UTC秒数,所以需要取出来,然后再转换为本地时区的秒数。

1
seconds = (last & 0x7FFFFFFF00000000) >> 32;

注意最高位是不能取的,尽管是unsigned,至于为什么要- 61533950这个是笔者在自己电脑上尝试出来的,找了很多资料不知是哪里的问题,还请各位知道的读者告诉我哈。

再来看看主函数:

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char* agrv[]) {
NtpClient ntp("129.6.15.28");
int n = 5;
while (n--) {
time_t tt = ntp.getTime();
boost::posix_time::ptime utc = from_time_t(tt);
std::cout << "Local Timestamp:" << time(0) << '\t' << "NTP Server:" << tt << "(" << to_simple_string(utc) << ")" << std::endl;
Sleep(10);
}
return 0;
}

这里进行5次NTP请求,并使用boost的to_simple_string转换UTC时间打印结果。

大概是这种效果:

收尾

同步时间一般都会想到找一个http api接口,本文主要是用了NTP协议。为了跨平台,上面的代码尽可能避免使用平台相关的宏和函数,只要稍作修改就能在各种平台下执行,也得益于boost这个强悍的准标准库给开发者带来的便利。

C++我也来写个工厂模式

Posted on 2014-08-28 |

工厂方法模式(Factory method pattern)是一种实现了“工厂”概念的面向对象设计模式。就像其他创建型模式一样,它也是处理在不指定对象具体类型的情况下创建对象的问题。工厂方法模式的实质是“定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。

以前是没有实现过工厂模式,这里我用到了template来创建类型不同的Products,内存管理这块没想到更好的办法来cleanup,打算是利用析构自动release,不过貌似到模版里就捉禁见肘了。。大家有什么高见?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <iostream>
#include <vector>

// ProductA

class ProductA
{
public:
void name();
};

void ProductA::name()
{
std::cout << "I'm Product A." << std::endl;
}

// ProductB

class ProductB
{
public:
void name();
};

void ProductB::name()
{
std::cout << "I'm Product B." << std::endl;
}


// Factory
template<typename T>
class Factory
{
public:
static T* create();
static void cleanup();
private:
Factory();
static std::vector<T*> objs_;
};
template<typename T>
std::vector<T*> Factory<T>::objs_;

template<typename T>
Factory<T>::Factory()
{
}

template<typename T>
void Factory<T>::cleanup()
{
for each (T* obj in objs_)
if (obj)
{
std::cout << "release " << obj << std::endl;
delete obj;
obj = nullptr;
}
objs_.clear();
}

template<typename T>
T* Factory<T>::create()
{
T * obj = new T;
objs_.push_back(obj);

return obj;
}

int main(int argc, char *argv[])
{
//create 10 ProductAs
for (size_t i = 1; i <= 10; i++)
{
auto pa = Factory<ProductA>::create();
pa->name();
}

//create 10 ProductBs
for (size_t i = 1; i <= 10; i++)
{
auto pb = Factory<ProductB>::create();
pb->name();
}

//free memory
Factory<ProductA>::cleanup();
Factory<ProductB>::cleanup();

return 0;

使用Golang实现简单Ping过程

Posted on 2014-08-11 |

引言

关于各种语言实现Ping已经是大家喜闻乐见的事情了,网络上利用Golang实现Ping已经有比较详细的代码示例,但大多是仅仅是实现了Request过程,而对Response的回显内容并没有做接收。而Ping程序不仅仅是发送一个ICMP,更重要的是如何接收并进行统计。

下面是网络上几篇关于Ping的实现代码:

https://github.com/paulstuart/ping/blob/master/ping.go

http://blog.csdn.net/gophers/article/details/21481447

http://blog.csdn.net/laputa73/article/details/17226337

本文借鉴了第二个链接里面的部分代码。

准备

  1. 安装最新的Go
    由于Google被墙的原因,如果没有VPN的话,就到这里下载:
    http://www.golangtc.com/download
  2. 使用任意文本编辑器,或者LiteIDE会比较方便编译和调试,下面是LiteIDE的下载地址
    https://github.com/visualfc/liteide

编码

要用到的package:

1
2
3
4
5
6
7
8
9
import (
"bytes"
"container/list"
"encoding/binary"
"fmt"
"net"
"os"
"time"
)
  1. 使用Golang提供的net包中的相关函数可以快速构造一个IP包并自定义其中一些关键参数,而不需要再自己手动填充IP报文。
  2. 使用encoding/binary包可以轻松获取结构体struct的内存数据并且可以规定字节序(这里要用网络字节序BigEndian),而不需要自己去转换字节序。之前的一片文中使用boost,还要自己去实现转换过程,详见:关于蹭网检查的原理及实现
  3. 使用container/list包,方便进行结果统计
  4. 使用time包实现耗时和超时处理

ICMP报文struct:

1
2
3
4
5
6
7
type ICMP struct {
Type uint8
Code uint8
Checksum uint16
Identifier uint16
SequenceNum uint16
}

Usage提示:

1
2
3
4
5
6
7
8
9
10
11
12
arg_num := len(os.Args)

if arg_num < 2 {
fmt.Print(
"Please runAs [super user] in [terminal].\n",
"Usage:\n",
"\tgoping url\n",
"\texample: goping www.baidu.com",
)
time.Sleep(5e9)
return
}

注意这个ping程序,包括之前的ARP程序都必须使用系统最高权限执行,所以这里先给出提示,使用time.Sleep(5e9),暂停5秒,是为了使双击执行者看到提示,避免控制台一闪而过。

关键net对象的创建和初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var (
icmp ICMP
laddr = net.IPAddr{IP: net.ParseIP("0.0.0.0")}
raddr, _ = net.ResolveIPAddr("ip", os.Args[1])
)

conn, err := net.DialIP("ip4:icmp", &laddr, raddr)

if err != nil {
fmt.Println(err.Error())
return
}

defer conn.Close()

net.DialIP表示生成一个IP报文,版本号是v4,协议是ICMP(这里字符串ip4:icmp会把IP报文的协议字段设为1表示ICMP协议),

源地址laddr可以是0.0.0.0也可以是自己的ip,这个并不影响ICMP的工作。

目的地址raddr是一个URL,这里使用Resolve进行DNS解析,注意返回值是一个指针,所以下面的DialIP方法中参数表示没有取地址符。

这样一个完整的IP报文就装配好了,我们并没有去操心IP中的其他一些字段,Go已经为我们处理好了。

通过返回的conn *net.IPConn对象可以进行后续操作。

defer conn.Close() 表示该函数将在Return时被执行,确保不会忘记关闭。

下面需要构造ICMP报文了:

1
2
3
4
5
6
7
8
9
10
11
icmp.Type = 8
icmp.Code = 0
icmp.Checksum = 0
icmp.Identifier = 0
icmp.SequenceNum = 0

var buffer bytes.Buffer
binary.Write(&buffer, binary.BigEndian, icmp)
icmp.Checksum = CheckSum(buffer.Bytes())
buffer.Reset()
binary.Write(&buffer, binary.BigEndian, icmp)

仍然非常简单,利用binary可以把一个结构体数据按照指定的字节序读到缓冲区里面,计算校验和后,再读进去。

检验和算法参考上面给出的URL中的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func CheckSum(data []byte) uint16 {
var (
sum uint32
length int = len(data)
index int
)
for length > 1 {
sum += uint32(data[index])<<8 + uint32(data[index+1])
index += 2
length -= 2
}
if length > 0 {
sum += uint32(data[index])
}
sum += (sum >> 16)

return uint16(^sum)
}

下面是Ping的Request过程,这里仿照Windows的ping,默认只进行4次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
fmt.Printf("\n正在 Ping %s 具有 0 字节的数据:\n", raddr.String())
recv := make([]byte, 1024)

statistic := list.New()
sended_packets := 0

for i := 4; i > 0; i-- {

if _, err := conn.Write(buffer.Bytes()); err != nil {
fmt.Println(err.Error())
return
}
sended_packets++
t_start := time.Now()

conn.SetReadDeadline((time.Now().Add(time.Second * 5)))
_, err := conn.Read(recv)

if err != nil {
fmt.Println("请求超时")
continue
}

t_end := time.Now()

dur := t_end.Sub(t_start).Nanoseconds() / 1e6

fmt.Printf("来自 %s 的回复: 时间 = %dms\n", raddr.String(), dur)

statistic.PushBack(dur)

//for i := 0; i < recvsize; i++ {
// if i%16 == 0 {
// fmt.Println("")
// }
// fmt.Printf("%.2x ", recv[i])
//}
//fmt.Println("")

}

“具有0字节的数据”表示ICMP报文中没有数据字段,这和Windows里面32字节的数据的略有不同。

conn.Write方法执行之后也就发送了一条ICMP请求,同时进行计时和计次。

conn.SetReadDeadline可以在未收到数据的指定时间内停止Read等待,并返回错误err,然后判定请求超时。否则,收到回应后,计算来回所用时间,并放入一个list方便后续统计。

注释部分内容是我在探索返回数据时的代码,读者可以试试看Read到的数据是哪个数据包的?

统计工作将在循环结束时进行,这里使用了defer其实是希望按了Ctrl+C之后能return执行,但是控制台确实不给力,直接给杀掉了。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
defer func() {
fmt.Println("")
//信息统计
var min, max, sum int64
if statistic.Len() == 0 {
min, max, sum = 0, 0, 0
} else {
min, max, sum = statistic.Front().Value.(int64), statistic.Front().Value.(int64), int64(0)
}

for v := statistic.Front(); v != nil; v = v.Next() {

val := v.Value.(int64)

switch {
case val < min:
min = val
case val > max:
max = val
}

sum = sum + val
}
recved, losted := statistic.Len(), sended_packets-statistic.Len()
fmt.Printf("%s 的 Ping 统计信息:\n 数据包:已发送 = %d,已接收 = %d,丢失 = %d (%.1f%% 丢失),\n往返行程的估计时间(以毫秒为单位):\n 最短 = %dms,最长 = %dms,平均 = %.0fms\n",
raddr.String(),
sended_packets, recved, losted, float32(losted)/float32(sended_packets)*100,
min, max, float32(sum)/float32(recved),
)
}()

统计过程注意类型的转换和格式化就行了。

全部代码就这些,执行结果大概是这个样子的:

注意每次Ping后都没有”休息”,不像Windows或者Linux的会停顿几秒再Ping下一轮。

收尾

Golang实现整个Ping比我想象中的还要简单很多,静态编译速度是十分快速,相比C而言,你需要更多得了解底层,甚至要从链路层开始,你需要写更多更复杂的代码来完成相同的工作,但究其根本,C语言仍然是鼻祖,功不可没,很多原理和思想都要继承和发展,这一点Golang做的很好。

关于蹭网检查的原理及实现

Posted on 2014-08-02 |

引言

网络十分普及的现在,几乎每家每户都用上了无线局域网, 但也时常因为路由器密码泄露或破解被别人蹭网,加之WiFi 万能钥匙等软件的流行, 越来越多人加入了蹭网大军, 也给不少小型局域网用户带来了烦恼. 目前许多安全软件厂商都在推出检查蹭网的小程序, 通过这样的程序可以十分便捷的看到哪些设备在使用局域网, 从而及时发现和采取应对措施, 为广大用户弥补了损失.

准备

笔者手里正好有一款由nisoft发布的检查蹭网的小程序, 叫做Wireless Network Watcher, 软件免费试用, 文末将给出下载地址. 本文将针对该款软件做分析. 其次是WireShark协议分析工具, 这个软件很常见, 感兴趣的话可以百度下载.

笔者的系统仍然是Windows8.1 Pro.

操作步骤

1.打开Wireless Network Watcher, 在Advanced Options中设置合适的Network adapter(可能含有其他网络设备的网段), 笔者主机所在网段是192.168.199.*

2.打开WireShark,选择合适的网卡然后启动监控

3.启动Wireless Network Watcher的扫描

4.等待扫描结束,然后停止两个软件

5.在WireShark中进行分析

分析

从Wireless Network Watcher的扫描结果来看,除了Router之外还发现了3个设备,一个是我的主机(Your Computer),一个是我的安卓手机,还有一个是HyFi智能无线路由器.

此外,包括MAC地址在内的IP地址, 设备名称Device Name等等都获取到了.

从Wireshark的抓包结果来看, 大多数是ARP协议, 在ARP应答报文中可以得到对应在线主机的MAC地址, 程序在收到应答后有一个DNS反向解析动作, 在DNS应答中又可以得到设备的Device Name,而这个Device Name应该是存在Router中的. 过滤其他干扰协议保留ARP和DNS. 可以十分明显的发现ARP请求报文的 Target IP address是从网段中0开始,一直到255, 也就是说软件扫描了整个网段, 发送了256个ARP广播用来查找在线的主机, 然后通过得到的IP地址向路由器发送DNS反向解析请求,用来获取设备的名称. 如图所示:

这里先借一段关于ARP协议的百科:

地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议。其功能是:主机将ARP请求广播到网络上的所有主机,并接收返回消息,确定目标IP地址的物理地址,同时将IP地址和硬件地址存入本机ARP缓存中,下次请求时直接查询ARP缓存。地址解析协议是建立在网络中各个主机互相信任的基础上的,网络上的主机可以自主发送ARP应答消息,其他主机收到应答报文时不会检测该报文的真实性就会将其记录在本地的ARP缓存中,这样攻击者就可以向目标主机发送伪ARP应答报文,使目标主机发送的信息无法到达相应的主机或到达错误的主机,构成一个ARP欺骗。ARP命令可用于查询本机ARP缓存中IP地址和MAC地址的对应关系、添加或删除静态对应关系等。

—- 百度百科

通过这段引用可以了解到ARP协议的Request是一个广播,发送到网络上所有主机,然后接收应答.

关于DNS的工作原理可以参考我的另一篇文: 《利用WireShark进行DNS协议分析》


现在让我们来分析一个ARP的请求和应答,以及一个DNS的请求和应答.

以第一个ARP为例:

第一个包是ARP请求,第二个是ARP应答,第三个是DNS请求,第四个是DNS应答,下面依次分析:

1. ARP请求

ARP请求的关键在于目标MAC尚未知道,因此全0,注意上层协议中的目标MAC是全f, 表示一个广播, 由于不同层的协议不同, 因此含义也不同. 请求解析的地址是192.168.199.1, opcode为 0x0001代表该ARP是一个Request.

ARP报文格式不在本文的讨论范围, 其本身也比较简单. 请读者自行百度.

2. ARP应答

当ARP请求广播后, 收到请求的主机检查Target IP address是否和自己相同,相同就回应一个ARP, 注意此时不再是一个广播了,而是定向的回应发送者. Sender MAC address字段里放有我们希望得到的目标MAC地址.

主机得到来自192.168.199.1的应答后, 取出MAC并记下IP地址, 为后面的DNS反向解析做准备.

其实到此为止, 就探测到了一个在线的主机, 完成了关键的侦测任务, 下面的DNS是软件本身为了优化用户体验, 向路由器查询一下设备名称而已. 此外,如果广播出去的ARP一定时间内没有收到回应,说明所探测的主机不在线.

3. DNS请求

主机收到ARP回应后, 利用IP地址进行DNS反向解析, 注意查询IP字段在报文中是逆序存放的. 目的地是路由器.

4. DNS应答

DNS应答报文中指出Domain Name是 Hiwifi.lan 这和软件上的Device Name一致.


实现

之前直接打算用boost的asio网络库来实现ARP的收发,搞了半天发现asio的rawsocket不能自定义实现这样的底层协议,网络上关于boost实现ARP的资料几乎没有,因此考虑了windows平台下的winpcap.

首先需要自己定义好arp以及ethernet报文(帧)的标准格式,为了简便起见,成员函数全部使用inline方式, 下面两个hpp分别定义了这两种格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//ethernet_header.hpp

#ifndef ETHERNET_HEADER_HPP
#define ETHERNET_HEADER_HPP

#include <istream>
#include <ostream>
#include "mac_address.hpp"

// Ethernet header
//
// The wire format of an Ethernet header is:
// 0 5 11 13
// +-----------------+-----------------+-----+
// |destination mac |source mac |type |
// |XX:XX:XX:XX:XX:XX|YY:YY:YY:YY:YY:YY|ZZ:ZZ|
// +-----------------+-----------------+-----+

class ethernet_header
{
public:
ethernet_header() { std::fill(rep_, rep_ + sizeof(rep_), 0); }

void dst(const MacAddr &mac_address) {
for (size_t i = 0; i < mac_address.size(); ++i) {
rep_[0 + i] = mac_address[i];
}
}

void src(const MacAddr &mac_address) {
for (size_t i = 0; i < mac_address.size(); ++i) {
rep_[6 + i] = mac_address[i];
}
}

void type(unsigned short n) { encode(12, 13, n); }

MacAddr dst() const {
MacAddr mac_address;
for (int i = 0; i < 6; ++i) {
mac_address.push_back(rep_[0 + i]);
}
return mac_address;
}

MacAddr src() const {
MacAddr mac_address;
for (int i = 0; i < 6; ++i) {
mac_address.push_back(rep_[6 + i]);
}
return mac_address;
}

unsigned short type() const { return decode(12, 13); }

friend std::istream& operator>>(std::istream& is, ethernet_header& header)
{
return is.read(reinterpret_cast<char*>(header.rep_), 14);
}

friend std::ostream& operator<<(std::ostream& os, const ethernet_header& header)
{
return os.write(reinterpret_cast<const char*>(header.rep_), 14);
}

private:
unsigned short decode(int a, int b) const
{
return (rep_[a] << 8) + rep_[b];
}

void encode(int a, int b, unsigned short n)
{
rep_[a] = static_cast<unsigned char>(n >> 8);
rep_[b] = static_cast<unsigned char>(n & 0xFF);
}

unsigned char rep_[14];
};

#endif // ETHERNET_HEADER_HPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//arp_header.hpp
#ifndef ARP_HEADER_HPP
#define ARP_HEADER_HPP

#include <istream>
#include <vector>
#include <boost/asio.hpp>

#include "mac_address.hpp"

// ARP header
//
// The wire format of an ARP header is:
//
// 0 8 16 31
// +-------------------------------+------------------------------+ ---
// | | | ^
// | Hardware type (HTYPE) | Protocol type (PTYPE) | |
// | | | |
// +---------------+---------------+------------------------------+ 4 bytes
// | | | | ^
// | Hard. len. | Proto. len. | Operation (OPER) | |
// | (HLEN) | (PLEN) | | |
// +-------------------------------+------------------------------+ 8 bytes
// | | ^
// | Sender hardware address (SHA) | |
// | | |
// +--------------------------------------------------------------+ 14 bytes
// | | ^
// | Sender protocol address (SPA) | |
// | | |
// +--------------------------------------------------------------+ 18 bytes
// | | ^
// | Target hardware address (THA) | |
// | | |
// +--------------------------------------------------------------+ 24 bytes
// | | ^
// | Target protocol address (TPA) | |
// | | |
// +--------------------------------------------------------------+ 28 bytes


class arp_header
{
public:

arp_header(){ std::fill(rep_, rep_ + sizeof(rep_), 0); }

//setter
void htype(unsigned short n){ encode(0, 1, n); }

void ptype(unsigned short n){ encode(2, 3, n); }

void hsize(unsigned char n){ rep_[4] = n; }

void psize(unsigned char n){ rep_[5] = n; }

void opcode(unsigned short n){ encode(6, 7, n); }

void sha(const MacAddr & mac){
for (size_t i = 0; i < mac.size(); ++i)
rep_[8 + i] = mac[i];
}

void spa(const boost::asio::ip::address_v4 &address){
auto bytes = address.to_bytes();
rep_[14] = bytes[0];
rep_[15] = bytes[1];
rep_[16] = bytes[2];
rep_[17] = bytes[3];
}

void tha(const MacAddr& mac){
for (size_t i = 0; i < mac.size(); ++i)
rep_[18 + i] = mac[i];
}

void tpa(const boost::asio::ip::address_v4 &address){
auto bytes = address.to_bytes();
rep_[24] = bytes[0];
rep_[25] = bytes[1];
rep_[26] = bytes[2];
rep_[27] = bytes[3];
}

//getter
unsigned short htype() const { return decode(0, 1); }

unsigned short ptype() const { return decode(2, 3); }

unsigned char hsize() const { return rep_[4]; }

unsigned char psize() const { return rep_[5]; }

unsigned short opcode() const { return decode(6, 7); }

MacAddr sha()const {
MacAddr mac;
for (size_t i = 0; i < 6; i++)
mac.push_back(rep_[8 + i]);
return mac;
}

boost::asio::ip::address_v4 spa() const {
boost::asio::ip::address_v4::bytes_type bytes
= {rep_[14], rep_[15], rep_[16], rep_[17]};
return boost::asio::ip::address_v4(bytes);
}

MacAddr tha()const{
MacAddr mac;
for (int i = 0; i < 6; ++i)
mac.push_back(rep_[18 + i]);
return mac;
}

boost::asio::ip::address_v4 tpa() const {
boost::asio::ip::address_v4::bytes_type bytes
= {rep_[24], rep_[25], rep_[26], rep_[27]};
return boost::asio::ip::address_v4(bytes);
}

//overloads

friend std::istream& operator>>(std::istream& is, arp_header& header)
{
return is.read(reinterpret_cast<char*>(header.rep_), 28);
}

friend std::ostream& operator<<(std::ostream& os, const arp_header& header)
{
return os.write(reinterpret_cast<const char*>(header.rep_), 28);
}

private:
void encode(int a, int b, unsigned short n)
{
rep_[a] = static_cast<unsigned char>(n >> 8);//取出高8位
rep_[b] = static_cast<unsigned char>(n & 0xff);//取出低8位
//相当于转换字节序,把小端格式转换为网络字节序
//例如 数 0x1234 在小端模式(Little-endian)中表示为:
//低地址---->高地址
//34 12
//网络序,大端模式(Big-endian)应该是:
//12 34
//该函数实现这个功能
}

unsigned short decode(int a, int b) const
{
return (rep_[a] << 8) + rep_[b];
//这个就是encode的反函数,把两个字节倒过来返回
}
unsigned char rep_[28];
};


#endif // ARP_HEADER_HPP

关于主机字节序(本例为小端)和网络字节序(大端)的转换过程可以参考上面代码中的注释.

实在贴不下这么多代码了,主函数代码包括所有本文涉及的hpp源代码请见下面的代码分享链接.

由于时间仓促,代码仅供学习交流,有很多遗留的问题尚未解决,但并不影响大家对整个实现过程的理解

运行结果

程序将扫描定义好的整个ip区段,发送ARP广播,然后接收响应,列出目标的MAC地址.具体实现请看下面的代码分享.

代码分享

http://www.oschina.net/code/snippet_580940_37722

相关下载

Wireless Network Watcher

利用WireShark分析由Ping产生的Internet 控制报文协议(ICMP)

Posted on 2014-07-15 |

ICMP是(Internet Control Message Protocol)Internet控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用

CMP为TCP/IP协议簇中的成员,工作在网络层,作用是在主机和路由器之间传递控制信息.

上文中Ping命令完成DNS域名解析任务后,随即利用得到的第一条主机资源记录(A记录)中的IP地址发送ICMP请求报文:

图中可以看到ICMP报文格式:

Type(类型)以及Code(代码)合起来说明ICMP报文类型.

–这里ICMP的类型是:请求(0x8)表示ping请求; 响应(0x0)表示ping响应

Checksum(校验和)包含整个ICMP报文数据的校验.

ID(标识符)和Seq(序列号)由发送端任意设置,响应报文返回相同的值,用于配对请求和响应.

–比如,在这个例子中,ID字段和Seq有两种表示方式:大端和小端,在请求和响应报文中,ID值始终不变;但是每发送一次请求,Seq就被加一.

随后的Data数据段长度不固定,ping命令的发送的Echo请求数据是32bytes的a~i字符序列,且没有终止0,

刚好印证了为什么Ping时会显示: “Ping xxx 具有32字节的数据:” 这里32字节的数据就是a~i字符序列.


ICMP响应报文中:

Type值是0x0,表示 ping reply,这一点显而易见的.

ID和Seq值和请求报文中的相同.

Data也是相同的.

在接收到响应之后同时计算出报文往返的时间,这里是Response time: 47.360ms

这样就完成了一次Ping


之后的三个Ping其实是重复上述操作,只不过Seq序号字段要自增.

今天就到这里,欢迎大家学习交流,其实我更希望得到大家的意见,谢谢点赞~

利用WireShark进行DNS协议分析

Posted on 2014-07-15 |

准备工作

系统是Windows 8.1Pro

分析工具是WireShark1.10.8 Stable Version

使用系统Ping命令发送ICMP报文.

开始工作

打开CMD.exe键入:

ping www.oschina.net

将自动进行域名解析,默认发送4个ICMP报文.

启动Wireshark,选择一个有效网卡,启动抓包.

在控制台回车执行完毕后停止监控.

分析阶段

截获的所有报文如下:

总得来看有两个DNS包(一次域名解析),和8个ICMP包(四次ping)

下面开始分析DNS的工作过程:

打开第一个包:

可以发现DNS为应用层协议,下层传输层采用UDP,再下层网络层是IP协议,然后是数据链路层的以太网帧.

需要关注的是应用层的实现也即DNS协议本身.

在此之前,可以从下层获得一些必要信息:

UDP(User Datagram Protocol)报文中: DNS的目的端口(Dst Port)是53

IPv4(Internet Protocol Version 4)报文中目的IP是192.168.1.1(局域网路由器)

由于IP报文在网络层进行路由选择,他会依次送给路由器而不是直接送给DNS服务器,这一点也十分容易理解,

第一个包是请求包,不可能直接包含DNS服务器地址.

展开DNS数据:

第一个是Transaction ID为标识字段,2字节,用于辨别DNS应答报文是哪个请求报文的响应.

第二个是Flags标志字段,2字节,每一位的含义不同,具体可以参考上面那个图,也可以看下面这个图:

QR: 查询/响应,1为响应,0为查询

Opcode: 查询或响应类型,这里0表示标准,1表示反向,2表示服务器状态请求

AA: 授权回答,在响应报文中有效,待会儿再看

TC: 截断,1表示超过512字节并已被截断,0表示没有发生截断

RD: 是否希望得到递归回答

RA: 响应报文中为1表示得到递归响应

zero: 全0保留字段

rcode: 返回码,在响应报文中,各取值的含义:

0 - 无差错

1 - 格式错误

2 - 域名服务器出现错误

3 - 域参照问题

4 - 查询类型不支持

5 - 被禁止

6 ~ 15 保留

紧接着标志位的是

Quetions(问题数),2字节,通常为1

Answer RRs(资源记录数),Authority RRs(授权资源记录数),Additional RRs(额外资源记录数)通常为0

字段Queries为查询或者响应的正文部分,分为Name Type Class

Name(查询名称):这里是ping后的参数,不定长度以0结束

Type(查询类型):2字节,这里是主机A记录.其各个取值的含义如下:

值        助记符         说明

 1         A                 IPv4地址。

 2         NS               名字服务器。

 5         CNAME        规范名称。定义主机的正式名字的别名。

 6         SOA             开始授权。标记一个区的开始。

 11       WKS             熟知服务。定义主机提供的网络服务。

 12       PTR               指针。把IP地址转化为域名。

 13       HINFO          主机信息。给出主机使用的硬件和操作系统的表述。

 15       MX               邮件交换。把邮件改变路由送到邮件服务器。

 28       AAAA           IPv6地址。

 252     AXFR            传送整个区的请求。

 255     ANY             对所有记录的请求。

Class(类):2字节,IN表示Internet数据,通常为1


下面是截获的第二个DNS包:

可以看到和第一个请求包相比,响应包多出了一个Answers字段,同时Flags字段每一位都有定义.

关注一下Flags中Answer RRs 为4 说明对应的Answers字段中将会出现4项解析结果.

Answers字段可以看成一个List,集合中每项为一个资源记录,除了上面提到过的Name,Type,Class之外,还有Time to

Live,Data length,Addr.

Time to Live(生存时间TTL):表示该资源记录的生命周期,从取出记录到抹掉记录缓存的时间,以秒为单位.这里是0x00 00 00 fd 合计253s.

Data length(资源数据长度):以字节为单位,这里的4表示IP地址的长度为4字节.也就是下面Addr字段的长度.

Addr(资源数据): 返回的IP地址,就是我们想要的结果.


可以发现有4条资源记录,4个不同的IP地址,说明域名 www.oschina.net 对应有4个IP地址,分别是:

112.124.5.74

219.136.249.194

61.145.122.155

121.9.213.124

CMD中显示的是第一条IP地址.我试了下直接访问上面各个地址的80端口(http),

第一个和第二个显示403 Forbidden

第三个和第四个显示404 Not Found

还有每个地址哦Server都不一样oscali,oscdb,liubc,ep2,第一个像阿里云服务器,第二个看起来像数据库的服务器,其他就不知道了…

Web服务器貌似是Tengine,

不知道为什么通过IP地址无法直接访问web站点,以后感兴趣再研究下哈哈


关于ICMP协议的报文分析将在之后的文章中给出.今天先到这吧.

最后,欢迎大家评论交流~特别是OSC在搞什么鬼.

实战利用WireShark对Telnet协议进行抓包分析

Posted on 2014-07-14 |

Telnet协议是TCP/IP协议族中的一员,是Internet远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用telnet程序,用它连接到服务器。终端使用者可以在telnet程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个telnet会话,必须输入用户名和密码来登录服务器。Telnet是常用的远程控制Web服务器的方法。

准备工作

  • 虚拟机Virtual Box(Telnet服务端)
    安装Windows XP SP3操作系统
    开启了Telnet服务
    添加了一个账户用于远程登录,用户名和密码都是micooz
  • 宿主机Windows 8.1 Pro(Telnet客户端)
    安装了分析工具Wireshark1.11.2
    安装了Telnet客户端程序

PS:虚拟机网卡选用桥接模式

操作流程

  1. 开启虚拟机和Wireshark
  2. 查看XP获得的ip地址,这里是192.168.199.134;网卡MAC地址:08:00:27:a0:9f:f8
    宿主机IP地址是192.168.199.154;网卡MAC地址:00:26:4d:a1:59:de
  3. 客户端主机启动cmd键入命令
    telnet 192.168.199.134 回车
  4. 显示是否发送密码
    回车
  5. 显示login:
    键入micooz回车
  6. 显示password:
    键入micooz回车
  7. 进入telnet客户端主界面命令行
  8. 键入net user回车
  9. 显示结果
  10. 关闭cmd
  11. 结束

开始分析

①建立连接(TCP三次握手)

步骤3执行后,telnet客户端开始工作,首先是与虚拟机中的服务端程序建立TCP连接,从抓取的数据包来看,首先关于本次分析的数据包是典型的TCP三次握手,如图所示:

由于是我第一次搞网络协议分析,就TCP的三次握手过程也做一个分析吧.

主机(192.168.199.154)发送一个连接请求到虚拟机(192.168.199.134),第一个TCP包的格式如图所示:

|以太网v2头|ipv4报文|TCP报文|

其中以太网v2头是由数据链路层加上去的:

1-6bytes是目的地址,也即虚拟机的网卡MAC,

7-12bytes是源地址,也即宿主机MAC.

13-14(0x0800)是上层协议,这里是IP

第二段是ipv4的报文,网际协议IP是工作在网络层,也就是数据链路层的上层,上图数据区选中部分就是ipv4数据,其格式为:

可以非常清楚的看到那些教科书上讲到的IPv4报文格式在实际中式如何表现出来的.

值得注意的是,IPv4报文中的源地址和目的地址是ip地址,版本号TCP是6,标志字段是010就是禁止下层分段,且是完整的报文

第三段是TCP报文,从上面两个可以知道,这个TCP包被层层包装,经过下面一层就相应的包装一层,第三段是经过传输层的数

据,TCP报文的格式为:

和书本上讲的格式基本一致,这里说明了TCP的源端口52456也就是宿主机建立连接开出来的端口,目的端口23显然是

telnet服务默认端口.

Sequence number同步序号4bytes,这里是0xa1 21 e2 42,但这里显示的是相对值0.

Acknowledgment number确认序号4bytes,为0,因为还是第一个握手包.

Header Length头长度32字节,滑动窗口大小8192字节(8MB),校验和,紧急指针为0.

Options选项12字节,其中包含最大传输单元MTU默认是1460bytes.

再来看看第二个TCP数据包,它是一个来自虚拟机的应答,按照三次握手的原则,这个数据包中TCP报文确认序号应该等于上

一个请求包中的同步序号+1,我们来看一下是不是:

Pack1. Seq = 0xa1 21 e2 42 Ack = 0x00 00 00 00

Pack2. Seq = 0x97 0f 37 11 Ack = 0xa1 21 e2 43

看下图更清楚:

显然如TCP规定的那样工作. Flags字段中也显示出两个包的标志位.第一个是SYN,第二个是SYN,ACK.

那么显然第三个包应该这样工作:

Pack1. Seq = 0xa1 21 e2 42 [Ack = 0x00 00 00 00]

Pack2. Seq = 0x97 0f 37 11 Ack = 0xa1 21 e2 43

Pack3. [Seq = 0xa1 21 e2 43] Ack = 0x97 0f 37 12

主机收到Pack2,取出其中Seq+1赋给Ack,然后给虚拟机做出应答. Pack1中的Ack和Pack3中的Seq在一次完整的三次

握手中似乎没起到什么作用,如果发生丢失可能会起作用吧,这里没条件去测试.

那么,虽然还没正式进入Telnet的核心,但是TCP三次握手的流程基本清晰了.下面小结一下:


1.TCP连接的建立通过三次握手完成.
2.TCP连接建立从传输层出发,TCP报文包装一个IP报头后形成一个IPv4报文经过网络层,然后再包装一个以太网帧头形成一个Ethernet帧通过数据链路层.
3.传输层的TCP报文含有Port端口地址; 网络层的IP报文中含有IP地址; 数据链路层中Ethernet帧含有MAC地址.可见层层
地址的不同之处,以及服务对象的不同之处.
4.三次握手规则就不再阐述了.

②身份确认

TCP连接建立后,主机和虚拟机相互交换一些信息,包括服务端的配置信息,主机的应答,是否需要登录等等,并且间断使用TCP

包保持连接.

当双方信息得到确认后,虚拟机发送欢迎信息(Welcome to Microsoft Telnet Service \r\n),主机做出应答

,随后又发送(\n\rlogin:),主机做出应答,然后同步一次,主机在CMD发生中断,接收用户输入,虚拟机等待用户输入.

主机输入一个字符就发送一个Telnet报文,然后远程返回一个应答,之后主机发送一个TCP报文.

三个一组:Telnet Telnet TCP

当然最后还有一个回车符\r\n也要产生三个数据包.

回车符发送之后,远端立即回送一个\n\rpassword:要求输入密码.

密码输入过程略有不同,一个字符产生两个包,一个是Telnet,一个是TCP.密码明文传输.

③命令执行和响应

完成密码输入后,服务端验证成功后发送一个Telnet报文询问是否Do Terminal Type开始执行命令行,主机客户端回应Will Terminal Type,将要执行,然后双方发送Suboption End消息,之后服务端放送欢迎消息,如图:

那么之后就可以开始输入命令了,我输入的是net user\r\n

和之前输入用户名的传输方法基本一样.两个Telnet一个TCP同步.

完成输入后回车,服务端执行命令并作出回应:

可以看到Administrator Guest HelpAssistant等字样,说明正确返回了执行结果.


关闭CMD窗口时,产生了4个TCP包,第一个TCP包设置标志位FIN告知本次通信结束,服务端回应一个TCP,

表示做好准备关闭连接,随后又发送一个TCP包设置FIN告知客户端要准备断开连接并断开,客户端应答一个表示已断开.通信结束.

这是典型的关闭TCP连接的过程.

总结

Telnet服务是建立在TCP基础之上的,保证数据的准确性.

建立连接后,每键入一个字符就要发送和应答,产生至少2个数据包,开销很大.

传统的Telnet由于密码明文传输的问题,帐号和密码等敏感资料容易会被窃听,因此很多服务器都会封锁Telnet服务,改

用更安全的SSH.

PS:本文是博主第一次尝试使用Wireshark进行网络协议分析,今后可能还会分析互联网其它一些协议,计算机网络

也是闲来无事自学的,本身非计算机专业,所以文中难免有专业术语或者概念性错误,还请批评指正!谢谢.

C++11新特性中的匿名函数Lambda表达式的汇编实现分析(二)

Posted on 2014-06-10 |
C++11新特性中的匿名函数Lambda表达式的汇编实现分析(一)

首先,让我们来看看以&方式进行变量捕获,同样没有参数和返回。

1
2
3
4
5
6
7
8
9
int main()
{
int a = 0xB;
auto lambda = [&]{
a = 0xA;
};
lambda();
return 0;
}

闭包中将main中a变量改写为0xA。

main中的关键汇编代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
int a = 0xB;
mov dword ptr [ebp-8],0Bh
auto lambda = [&]{
a = 0xA;
};
lea eax,[ebp-8]
push eax
lea ecx,[ebp-14h]
call 002D1BE0
lambda();
lea ecx,[ebp-14h]
call 002D1C20
return 0;

同样的,进入闭包前要调用一个拷贝函数。

002D1BE0 内:

1
2
3
4
5
6
7
8
9
10
11
12
pop         ecx  
mov dword ptr [ebp-8],ecx
mov eax,dword ptr [ebp-8]
mov ecx,dword ptr [ebp+8]
mov dword ptr [eax],ecx
mov eax,dword ptr [ebp-8]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 4

和前面一篇文章中的代码基本一致,但是有两个地方不同,

上文写到:

1
2
3
4
5
6
7
pop         ecx  
mov dword ptr [ebp-8],ecx
mov eax,dword ptr [ebp-8]
mov ecx,dword ptr [ebp+8]
mov edx,dword ptr [ecx]
mov dword ptr [eax],edx
mov eax,dword ptr [ebp-8]

注意黑体部分,若采用[=]的捕获方式,那么将通过寄存器edx拷贝原变量的值;

若采用[&]方式,则直接通过ecx拷贝原变量的地址,而不取出值。

闭包内:

1
2
3
4
5
6
7
8
9
10
11
12
13
pop         ecx  
mov dword ptr [ebp-8],ecx
a = 0xA;
mov eax,dword ptr [ebp-8]
mov ecx,dword ptr [eax]
mov dword ptr [ecx],0Ah
};
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret

对a进行赋值,直接是:

*this = 0xA;

因为事先this就取到了a的地址。

可以发现,按引用捕获实际上就如同函数参数传递引用,传递的是一个地址值,而不创建对象副本(可以和上一篇文中的内容比较)。

C++11标准中对Lambda表达式的捕获内容还有一些特定支持,比如可以以指定的方式捕获指定的变量:

1
2
3
4
5
6
7
8
9
10
int main()
{
int a = 0xB;
bool b = true;
auto lambda = [&a,b]{
a = b;
};
lambda();
return 0;
}

上面的代码对a进行引用捕获,对b按值捕获。根据前面分析的结果,可以预见,a的地址和b的值将被拷贝以供闭包函数使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int a = 0xB;
mov dword ptr [ebp-8],0Bh
bool b = true;
mov byte ptr [ebp-11h],1
auto lambda = [&a,b]{
a = b;
};
lea eax,[ebp-11h]
push eax
lea ecx,[ebp-8]
push ecx
lea ecx,[ebp-24h]
call 00222060
lambda();
lea ecx,[ebp-24h]
lambda();
call 00221C20
return 0;

调用Lambda之前,先调用复制函数,传入两个参数,&a和&b,而this被放在main的[ebp-24h]中。

复制函数或者叫准备函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pop         ecx  
mov dword ptr [ebp-8],ecx
mov eax,dword ptr [ebp-8]
mov ecx,dword ptr [ebp+8]
mov dword ptr [eax],ecx
mov eax,dword ptr [ebp-8]
mov ecx,dword ptr [ebp+0Ch]
mov dl,byte ptr [ecx]
mov byte ptr [eax+4],dl
mov eax,dword ptr [ebp-8]
pop edi
pop esi
pop ebx
mov esp,ebp
pop ebp
ret 8

[eax] 就是 *this,也即a

[eax+4] 就是 *(this+4),也即b

从内存图可以清楚的看到,a的地址被记录,b的值被复制

闭包函数内:

1
2
3
4
5
6
7
8
9
pop         ecx  
mov dword ptr [ebp-8],ecx
a = b;
mov eax,dword ptr [ebp-8]
movzx ecx,byte ptr [eax+4]

mov edx,dword ptr [ebp-8]
mov eax,dword ptr [edx]
mov dword ptr [eax],ecx

b的值是从[eax+4]也即this+4中取出,而a在this中,其实就是:

(this) = (this+4);

可以看到闭包内通过this作为基址,对闭包外的变量进行偏移访问。

下一篇中,我将对具有参数和返回值的Lambda表达式进行分析。

12345
Micooz Lee

Micooz Lee

FullStack JavaScript Engineer

50 posts
17 tags
RSS
GitHub E-Mail Twitter Instagram
Links
  • 哞菇菌
  • 海胖博客
  • 音風の部屋
  • DIYgod
  • BlueCocoa
  • ShinCurry
  • codelover
  • ChionTang
  • Rakume Hayashi
  • POJO
© 2018 Micooz Lee
Powered by Hexo v3.7.1
|
Theme — NexT.Pisces v6.1.0
0%